Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 10:33:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 10:33:21 +0300
commit36a59d088eca61b834191dacea009677a96c052f (patch)
treee4f33972dab5d8ef79e3944a9f403035fceea43f /app/assets/javascripts/pipelines
parenta1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff)
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipelines')
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue60
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue151
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue17
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue73
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue111
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/utils.js33
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_tabs.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue220
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue29
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue16
-rw-r--r--app/assets/javascripts/pipelines/constants.js47
-rw-r--r--app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql12
-rw-r--r--app/assets/javascripts/pipelines/graphql/provider.js9
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql41
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js14
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_dag.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js36
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_header.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_notification.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_tabs.js27
-rw-r--r--app/assets/javascripts/pipelines/pipelines_index.js8
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js6
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/constants.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/getters.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js1
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js14
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js1
-rw-r--r--app/assets/javascripts/pipelines/utils.js19
39 files changed, 974 insertions, 153 deletions
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 16fb931ec2b..475dd3bf36e 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -238,7 +238,7 @@ export default {
</div>
</template>
<template v-if="dagDocPath" #actions>
- <gl-button :href="dagDocPath" target="__blank" variant="success">
+ <gl-button :href="dagDocPath" target="_blank" variant="confirm">
{{ $options.emptyStateTexts.button }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index 0b59612b25c..85ca52f633e 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -15,4 +15,8 @@ export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
export const SINGLE_JOB = 'single_job';
export const JOB_DROPDOWN = 'job_dropdown';
+export const BUILD_KIND = 'BUILD';
+export const BRIDGE_KIND = 'BRIDGE';
+
+export const ACTION_FAILURE = 'action_failure';
export const IID_FAILURE = 'missing_iid';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 015f0519c72..31a34ab4fb5 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -233,6 +233,7 @@ export default {
:view-type="viewType"
@downstreamHovered="setSourceJob"
@pipelineExpandToggle="togglePipelineExpanded"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
@scrollContainer="slidePipelineContainer"
@error="onError"
/>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index 534ad25a35d..f822e2c0874 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -8,7 +8,7 @@ import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
import { reportToSentry, reportMessageToSentry } from '../../utils';
-import { IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
+import { ACTION_FAILURE, IID_FAILURE, LAYER_VIEW, STAGE_VIEW, VIEW_TYPE_KEY } from './constants';
import PipelineGraph from './graph_component.vue';
import GraphViewSelector from './graph_view_selector.vue';
import {
@@ -57,13 +57,29 @@ export default {
showLinks: false,
};
},
- errorTexts: {
- [DRAW_FAILURE]: __('An error occurred while drawing job relationship links.'),
- [IID_FAILURE]: __(
- 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
- ),
- [LOAD_FAILURE]: __('We are currently unable to fetch data for this pipeline.'),
- [DEFAULT]: __('An unknown error occurred while loading this graph.'),
+ errors: {
+ [ACTION_FAILURE]: {
+ text: __('An error occurred while performing this action.'),
+ variant: 'danger',
+ },
+ [DRAW_FAILURE]: {
+ text: __('An error occurred while drawing job relationship links.'),
+ variant: 'danger',
+ },
+ [IID_FAILURE]: {
+ text: __(
+ 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
+ ),
+ variant: 'info',
+ },
+ [LOAD_FAILURE]: {
+ text: __('Currently unable to fetch data for this pipeline.'),
+ variant: 'danger',
+ },
+ [DEFAULT]: {
+ text: __('An unknown error occurred while loading this graph.'),
+ variant: 'danger',
+ },
},
apollo: {
callouts: {
@@ -154,28 +170,12 @@ export default {
},
computed: {
alert() {
- switch (this.alertType) {
- case DRAW_FAILURE:
- return {
- text: this.$options.errorTexts[DRAW_FAILURE],
- variant: 'danger',
- };
- case IID_FAILURE:
- return {
- text: this.$options.errorTexts[IID_FAILURE],
- variant: 'info',
- };
- case LOAD_FAILURE:
- return {
- text: this.$options.errorTexts[LOAD_FAILURE],
- variant: 'danger',
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT],
- variant: 'danger',
- };
- }
+ const { errors } = this.$options;
+
+ return {
+ text: errors[this.alertType]?.text ?? errors[DEFAULT].text,
+ variant: errors[this.alertType]?.variant ?? errors[DEFAULT].variant,
+ };
},
configPaths() {
return {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index f69b25dfa7c..362571930d6 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { sprintf, __ } from '~/locale';
@@ -7,7 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { SINGLE_JOB } from './constants';
+import { BRIDGE_KIND, SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -35,11 +35,16 @@ import { SINGLE_JOB } from './constants';
*/
export default {
+ i18n: {
+ bridgeBadgeText: __('Trigger job'),
+ unauthorizedTooltip: __('You are not authorized to run this manual job'),
+ },
hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
components: {
ActionComponent,
CiIcon,
JobNameComponent,
+ GlBadge,
GlLink,
},
directives: {
@@ -113,6 +118,12 @@ export default {
isSingleItem() {
return this.type === SINGLE_JOB;
},
+ isBridge() {
+ return this.kind === BRIDGE_KIND;
+ },
+ kind() {
+ return this.job?.kind || '';
+ },
nameComponent() {
return this.hasDetails ? 'gl-link' : 'div';
},
@@ -187,6 +198,7 @@ export default {
[this.$options.hoverClass]:
this.relatedDownstreamHovered || this.relatedDownstreamExpanded,
},
+ { 'gl-rounded-lg': this.isBridge },
this.cssClassJobName,
];
},
@@ -213,9 +225,6 @@ export default {
this.$emit('pipelineActionRequestComplete');
},
},
- i18n: {
- unauthorizedTooltip: __('You are not authorized to run this manual job'),
- },
};
</script>
<template>
@@ -253,6 +262,9 @@ export default {
</div>
</div>
</div>
+ <gl-badge v-if="isBridge" class="gl-mt-3" variant="info" size="sm">
+ {{ $options.i18n.bridgeBadgeText }}
+ </gl-badge>
</component>
<action-component
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index d59802196af..9f76d4cec50 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,10 +1,22 @@
<script>
-import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlBadge,
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
+import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
+import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { PIPELINE_GRAPHQL_TYPE } from '../../constants';
import { reportToSentry } from '../../utils';
-import { DOWNSTREAM, UPSTREAM } from './constants';
+import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants';
export default {
directives: {
@@ -16,7 +28,14 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
+ GlTooltip,
},
+ styles: {
+ actionSizeClasses: ['gl-h-7 gl-w-7'],
+ flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
+ flatRightBorder: ['gl-rounded-bottom-right-none!', 'gl-rounded-top-right-none!'],
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
columnTitle: {
type: String,
@@ -39,15 +58,44 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ hasActionTooltip: false,
+ isActionLoading: false,
+ };
+ },
computed: {
- buttonBorderClass() {
- return this.isUpstream ? 'gl-border-r-1!' : 'gl-border-l-1!';
+ action() {
+ if (this.glFeatures?.downstreamRetryAction && this.isDownstream) {
+ if (this.isCancelable) {
+ return {
+ icon: 'cancel',
+ method: this.cancelPipeline,
+ ariaLabel: __('Cancel downstream pipeline'),
+ };
+ } else if (this.isRetryable) {
+ return {
+ icon: 'retry',
+ method: this.retryPipeline,
+ ariaLabel: __('Retry downstream pipeline'),
+ };
+ }
+ }
+
+ return {};
+ },
+ buttonBorderClasses() {
+ return this.isUpstream
+ ? ['gl-border-r-0!', ...this.$options.styles.flatRightBorder]
+ : ['gl-border-l-0!', ...this.$options.styles.flatLeftBorder];
},
buttonId() {
return `js-linked-pipeline-${this.pipeline.id}`;
},
- cardSpacingClass() {
- return this.isDownstream ? 'gl-pr-0' : '';
+ cardClasses() {
+ return this.isDownstream
+ ? this.$options.styles.flatRightBorder
+ : this.$options.styles.flatLeftBorder;
},
expandedIcon() {
if (this.isUpstream) {
@@ -64,9 +112,21 @@ export default {
flexDirection() {
return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
},
+ graphqlPipelineId() {
+ return convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, this.pipeline.id);
+ },
+ hasUpdatePipelinePermissions() {
+ return Boolean(this.pipeline?.userPermissions?.updatePipeline);
+ },
+ isCancelable() {
+ return Boolean(this.pipeline?.cancelable && this.hasUpdatePipelinePermissions);
+ },
isDownstream() {
return this.type === DOWNSTREAM;
},
+ isRetryable() {
+ return Boolean(this.pipeline?.retryable && this.hasUpdatePipelinePermissions);
+ },
isSameProject() {
return !this.pipeline.multiproject;
},
@@ -93,13 +153,19 @@ export default {
projectName() {
return this.pipeline.project.name;
},
+ showAction() {
+ return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
+ },
+ showCardTooltip() {
+ return !this.hasActionTooltip;
+ },
sourceJobName() {
return this.pipeline.sourceJob?.name ?? '';
},
sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
},
- tooltipText() {
+ cardTooltipText() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`;
},
@@ -108,6 +174,26 @@ export default {
reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
},
methods: {
+ cancelPipeline() {
+ this.executePipelineAction(CancelPipelineMutation);
+ },
+ async executePipelineAction(mutation) {
+ try {
+ this.isActionLoading = true;
+
+ await this.$apollo.mutate({
+ mutation,
+ variables: {
+ id: this.graphqlPipelineId,
+ },
+ });
+ this.$emit('refreshPipelineGraph');
+ } catch {
+ this.$emit('error', { type: ACTION_FAILURE });
+ } finally {
+ this.isActionLoading = false;
+ }
+ },
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
@@ -122,6 +208,12 @@ export default {
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
},
+ retryPipeline() {
+ this.executePipelineAction(RetryPipelineMutation);
+ },
+ setActionTooltip(flag) {
+ this.hasActionTooltip = flag;
+ },
},
};
</script>
@@ -129,33 +221,48 @@ export default {
<template>
<div
ref="linkedPipeline"
- v-gl-tooltip
- class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
+ class="gl-h-full gl-display-flex!"
:class="flexDirection"
- :title="tooltipText"
data-qa-selector="linked_pipeline_container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
- <div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass">
- <div class="gl-display-flex gl-pr-3">
- <ci-status
- v-if="!pipelineIsLoading"
- :status="pipelineStatus"
- :size="24"
- css-classes="gl-top-0 gl-pr-2"
- />
+ <gl-tooltip v-if="showCardTooltip" :target="() => $refs.linkedPipeline">
+ {{ cardTooltipText }}
+ </gl-tooltip>
+ <div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
+ <div class="gl-display-flex gl-gap-x-3">
+ <ci-status v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" css-classes="" />
<div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
- <div class="gl-display-flex gl-flex-direction-column gl-downstream-pipeline-job-width">
+ <div
+ class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
+ >
<span class="gl-text-truncate" data-testid="downstream-title">
{{ downstreamTitle }}
</span>
<div class="gl-text-truncate">
- <gl-link class="gl-text-blue-500!" :href="pipeline.path" data-testid="pipelineLink"
+ <gl-link
+ class="gl-text-blue-500! gl-font-sm"
+ :href="pipeline.path"
+ data-testid="pipelineLink"
>#{{ pipeline.id }}</gl-link
>
</div>
</div>
+ <gl-button
+ v-if="showAction"
+ v-gl-tooltip
+ :title="action.ariaLabel"
+ :loading="isActionLoading"
+ :icon="action.icon"
+ class="gl-rounded-full!"
+ :class="$options.styles.actionSizeClasses"
+ :aria-label="action.ariaLabel"
+ @click="action.method"
+ @mouseover="setActionTooltip(true)"
+ @mouseout="setActionTooltip(false)"
+ />
+ <div v-else :class="$options.styles.actionSizeClasses"></div>
</div>
<div class="gl-pt-2">
<gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label">
@@ -166,8 +273,8 @@ export default {
<div class="gl-display-flex">
<gl-button
:id="buttonId"
- class="gl-shadow-none! gl-rounded-0!"
- :class="`js-pipeline-expand-${pipeline.id} ${buttonBorderClass}`"
+ class="gl-border! gl-shadow-none! gl-rounded-lg!"
+ :class="[`js-pipeline-expand-${pipeline.id}`, buttonBorderClasses]"
:icon="expandedIcon"
:aria-label="__('Expand pipeline')"
data-testid="expand-pipeline-button"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 3c1208afbf0..b06c2f15042 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -66,14 +66,13 @@ export default {
columnClass() {
const positionValues = {
right: 'gl-ml-6',
- left: 'gl-mr-6',
+ left: 'gl-mx-6',
};
+
return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
},
computedTitleClasses() {
- const positionalClasses = this.isUpstream
- ? ['gl-w-full', 'gl-text-right', 'gl-linked-pipeline-padding']
- : [];
+ const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : [];
return [...this.$options.titleClasses, ...positionalClasses];
},
@@ -202,7 +201,7 @@ export default {
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id"
- class="gl-display-flex gl-mb-4"
+ class="gl-display-flex gl-mb-3"
:class="{ 'gl-flex-direction-row-reverse': isUpstream }"
>
<linked-pipeline
@@ -215,6 +214,7 @@ export default {
@downstreamHovered="onDownstreamHovered"
@pipelineClicked="onPipelineClick(pipeline)"
@pipelineExpandToggle="onPipelineExpandToggle"
+ @refreshPipelineGraph="$emit('refreshPipelineGraph')"
/>
<div
v-if="showContainer(pipeline.id)"
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 04b78b8aa23..37878f3fb6d 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -93,6 +93,7 @@ export default {
data() {
return {
pipeline: null,
+ failureMessages: [],
failureType: null,
isCanceling: false,
isRetrying: false,
@@ -159,8 +160,9 @@ export default {
},
},
methods: {
- reportFailure(errorType) {
+ reportFailure(errorType, errorMessages = []) {
this.failureType = errorType;
+ this.failureMessages = errorMessages;
},
async postPipelineAction(name, mutation) {
try {
@@ -176,7 +178,7 @@ export default {
if (errors.length > 0) {
this.isRetrying = false;
- this.reportFailure(POST_FAILURE);
+ this.reportFailure(POST_FAILURE, errors);
} else {
await this.$apollo.queries.pipeline.refetch();
if (!this.isFinished) {
@@ -214,7 +216,7 @@ export default {
});
if (errors.length > 0) {
- this.reportFailure(DELETE_FAILURE);
+ this.reportFailure(DELETE_FAILURE, errors);
this.isDeleting = false;
} else {
redirectTo(setUrlFragment(this.paths.pipelinesPath, 'delete_success'));
@@ -231,9 +233,11 @@ export default {
</script>
<template>
<div class="js-pipeline-header-container">
- <gl-alert v-if="hasError" :variant="failure.variant" :dismissible="false">{{
- failure.text
- }}</gl-alert>
+ <gl-alert v-if="hasError" :title="failure.text" :variant="failure.variant" :dismissible="false">
+ <div v-for="(failureMessage, index) in failureMessages" :key="`failure-message-${index}`">
+ {{ failureMessage }}
+ </div>
+ </gl-alert>
<ci-header
v-if="shouldRenderContent"
:status="pipeline.detailedStatus"
@@ -261,6 +265,7 @@ export default {
v-if="canCancelPipeline"
:loading="isCanceling"
:disabled="isCanceling"
+ class="gl-ml-3"
variant="danger"
data-testid="cancelPipeline"
@click="cancelPipeline()"
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
new file mode 100644
index 00000000000..9e886fd7a48
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import GetFailedJobsQuery from '../../graphql/queries/get_failed_jobs.query.graphql';
+import { prepareFailedJobs } from './utils';
+import FailedJobsTable from './failed_jobs_table.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ FailedJobsTable,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ pipelineIid: {
+ default: '',
+ },
+ },
+ props: {
+ failedJobsSummary: {
+ type: Array,
+ required: true,
+ },
+ },
+ apollo: {
+ failedJobs: {
+ query: GetFailedJobsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ pipelineIid: this.pipelineIid,
+ };
+ },
+ update({ project }) {
+ if (project?.pipeline?.jobs?.nodes) {
+ return project.pipeline.jobs.nodes.map((job) => {
+ return { normalizedId: getIdFromGraphQLId(job.id), ...job };
+ });
+ }
+ return [];
+ },
+ result() {
+ this.preparedFailedJobs = prepareFailedJobs(this.failedJobs, this.failedJobsSummary);
+ },
+ error() {
+ createFlash({ message: s__('Jobs|There was a problem fetching the failed jobs.') });
+ },
+ },
+ },
+ data() {
+ return {
+ failedJobs: [],
+ preparedFailedJobs: [],
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.failedJobs.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="loading" size="lg" class="gl-mt-4" />
+ <failed-jobs-table v-else :failed-jobs="preparedFailedJobs" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
new file mode 100644
index 00000000000..1c646bdf3d6
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -0,0 +1,111 @@
+<script>
+import { GlButton, GlLink, GlSafeHtmlDirective, GlTableLite } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import createFlash from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
+import { DEFAULT_FIELDS } from '../../constants';
+
+export default {
+ fields: DEFAULT_FIELDS,
+ retry: __('Retry'),
+ components: {
+ CiBadge,
+ GlButton,
+ GlLink,
+ GlTableLite,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ failedJobs: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ async retryJob(id) {
+ try {
+ const {
+ data: {
+ jobRetry: { errors, job },
+ },
+ } = await this.$apollo.mutate({
+ mutation: RetryFailedJobMutation,
+ variables: { id },
+ });
+ if (errors.length > 0) {
+ this.showErrorMessage();
+ } else {
+ redirectTo(job.detailedStatus.detailsPath);
+ }
+ } catch {
+ this.showErrorMessage();
+ }
+ },
+ canRetryJob(job) {
+ return job.retryable && job.userPermissions.updateBuild;
+ },
+ showErrorMessage() {
+ createFlash({ message: s__('Job|There was a problem retrying the failed job.') });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-table-lite :items="failedJobs" :fields="$options.fields" stacked="lg" fixed>
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+
+ <template #cell(name)="{ item }">
+ <div
+ class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
+ >
+ <ci-badge :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
+ <div class="gl-text-truncate">
+ <gl-link
+ :href="item.detailedStatus.detailsPath"
+ class="gl-font-weight-bold gl-text-gray-900!"
+ >
+ {{ item.name }}
+ </gl-link>
+ </div>
+ </div>
+ </template>
+
+ <template #cell(stage)="{ item }">
+ <div class="gl-text-truncate">
+ <span>{{ item.stage.name }}</span>
+ </div>
+ </template>
+
+ <template #cell(failure)="{ item }">
+ <span>{{ item.failure }}</span>
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <gl-button
+ v-if="canRetryJob(item)"
+ icon="repeat"
+ :title="$options.retry"
+ :aria-label="$options.retry"
+ @click="retryJob(item.id)"
+ />
+ </template>
+
+ <template #row-details="{ item }">
+ <pre
+ v-if="item.userPermissions.readBuild"
+ class="gl-w-full gl-text-left gl-border-none"
+ data-testid="job-log"
+ >
+ <code v-safe-html="item.failureSummary" class="gl-reset-bg gl-p-0" >
+ </code>
+ </pre>
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/utils.js b/app/assets/javascripts/pipelines/components/jobs/utils.js
new file mode 100644
index 00000000000..c8414d44d14
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/jobs/utils.js
@@ -0,0 +1,33 @@
+/*
+ We get the failure and failure summary from Rails which has
+ a summary failure log. Here we combine that data with the data
+ from GraphQL to display the log.
+
+ failedJobs is from GraphQL
+ failedJobsSummary is from Rails
+ */
+
+export const prepareFailedJobs = (failedJobs = [], failedJobsSummary = []) => {
+ const combinedJobs = [];
+
+ if (failedJobs.length > 0 && failedJobsSummary.length > 0) {
+ failedJobs.forEach((failedJob) => {
+ const foundJob = failedJobsSummary.find(
+ (failedJobSummary) => failedJob.normalizedId === failedJobSummary.id,
+ );
+
+ if (foundJob) {
+ combinedJobs.push({
+ ...failedJob,
+ failure: foundJob?.failure,
+ failureSummary: foundJob?.failure_summary,
+ // this field is needed for the slot row-details
+ // on the failed_jobs_table.vue component
+ _showDetails: true,
+ });
+ }
+ });
+ }
+
+ return combinedJobs;
+};
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
index 62c785d7ad2..66d30c10362 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
@@ -1,6 +1,7 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
+import { failedJobsTabName, jobsTabName, needsTabName, testReportTabName } from '../constants';
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
import Dag from './dag/dag.vue';
import JobsApp from './jobs/jobs_app.vue';
@@ -16,6 +17,12 @@ export default {
testsTitle: __('Tests'),
},
},
+ tabNames: {
+ needs: needsTabName,
+ jobs: jobsTabName,
+ failures: failedJobsTabName,
+ tests: testReportTabName,
+ },
components: {
Dag,
GlTab,
@@ -25,24 +32,47 @@ export default {
PipelineGraphWrapper,
TestReports,
},
+ inject: ['defaultTabValue'],
+ methods: {
+ isActive(tabName) {
+ return tabName === this.defaultTabValue;
+ },
+ },
};
</script>
<template>
<gl-tabs>
- <gl-tab :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
+ <gl-tab ref="pipelineTab" :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
<pipeline-graph-wrapper />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.needsTitle" data-testid="dag-tab">
+ <gl-tab
+ ref="dagTab"
+ :title="$options.i18n.tabs.needsTitle"
+ :active="isActive($options.tabNames.needs)"
+ data-testid="dag-tab"
+ >
<dag />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.jobsTitle" data-testid="jobs-tab">
+ <gl-tab
+ :title="$options.i18n.tabs.jobsTitle"
+ :active="isActive($options.tabNames.jobs)"
+ data-testid="jobs-tab"
+ >
<jobs-app />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.failedJobsTitle" data-testid="failed-jobs-tab">
+ <gl-tab
+ :title="$options.i18n.tabs.failedJobsTitle"
+ :active="isActive($options.tabNames.failures)"
+ data-testid="failed-jobs-tab"
+ >
<failed-jobs-app />
</gl-tab>
- <gl-tab :title="$options.i18n.tabs.testsTitle" data-testid="tests-tab">
+ <gl-tab
+ :title="$options.i18n.tabs.testsTitle"
+ :active="isActive($options.tabNames.tests)"
+ data-testid="tests-tab"
+ >
<test-reports />
</gl-tab>
<slot></slot>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
index 5a9c85a0f10..3bbdfc73e1b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
@@ -1,7 +1,9 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
+import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from './empty_state/pipelines_ci_templates.vue';
+import IosTemplates from './empty_state/ios_templates.vue';
export default {
i18n: {
@@ -10,7 +12,9 @@ export default {
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
+ GitlabExperiment,
PipelinesCiTemplates,
+ IosTemplates,
},
props: {
emptyStateSvgPath: {
@@ -21,26 +25,24 @@ export default {
type: Boolean,
required: true,
},
- ciRunnerSettingsPath: {
+ registrationToken: {
type: String,
required: false,
default: null,
},
- anyRunnersAvailable: {
- type: Boolean,
- required: false,
- default: true,
- },
},
};
</script>
<template>
<div>
- <pipelines-ci-templates
- v-if="canSetCi"
- :ci-runner-settings-path="ciRunnerSettingsPath"
- :any-runners-available="anyRunnersAvailable"
- />
+ <gitlab-experiment v-if="canSetCi" name="ios_specific_templates">
+ <template #control>
+ <pipelines-ci-templates />
+ </template>
+ <template #candidate>
+ <ios-templates :registration-token="registrationToken" />
+ </template>
+ </gitlab-experiment>
<gl-empty-state
v-else
title=""
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
index 3b312e78d11..64d4414eb94 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
@@ -12,15 +12,31 @@ export default {
},
mixins: [Tracking.mixin()],
inject: ['pipelineEditorPath', 'suggestedCiTemplates'],
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ filterTemplates: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
data() {
- const templates = this.suggestedCiTemplates.map(({ name, logo }) => {
- return {
- name,
- logo,
- link: mergeUrlParams({ template: name }, this.pipelineEditorPath),
- description: sprintf(this.$options.i18n.description, { name }),
- };
- });
+ const templates = this.suggestedCiTemplates
+ .filter(
+ (template) => !this.filterTemplates.length || this.filterTemplates.includes(template.name),
+ )
+ .map(({ name, logo, title }) => {
+ return {
+ name: title || name,
+ logo,
+ link: mergeUrlParams({ template: name }, this.pipelineEditorPath),
+ description: sprintf(this.$options.i18n.description, { name: title || name }),
+ };
+ });
return {
templates,
@@ -34,7 +50,9 @@ export default {
},
},
i18n: {
- description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'),
+ description: s__(
+ 'Pipelines|Continuous integration and deployment template to test and deploy your %{name} project.',
+ ),
cta: s__('Pipelines|Use template'),
},
AVATAR_SHAPE_OPTION_RECT,
@@ -67,6 +85,7 @@ export default {
</div>
</div>
<gl-button
+ :disabled="disabled"
category="primary"
variant="confirm"
:href="template.link"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
new file mode 100644
index 00000000000..8ff311e90e7
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
@@ -0,0 +1,220 @@
+<script>
+import { GlButton, GlCard, GlSprintf, GlLink, GlPopover, GlModalDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import apolloProvider from '~/pipelines/graphql/provider';
+import CiTemplates from './ci_templates.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlCard,
+ GlSprintf,
+ GlLink,
+ GlPopover,
+ RunnerInstructionsModal,
+ CiTemplates,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: ['pipelineEditorPath', 'iosRunnersAvailable'],
+ props: {
+ registrationToken: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ apolloProvider,
+ iOSTemplateName: 'iOS-Fastlane',
+ modalId: 'runner-instructions-modal',
+ runnerDocsLink: 'https://docs.gitlab.com/runner/install/osx',
+ whatElseLink: helpPagePath('ci/index.md'),
+ i18n: {
+ title: s__('Pipelines|Get started with GitLab CI/CD'),
+ subtitle: s__('Pipelines|Building for iOS?'),
+ explanation: s__("Pipelines|We'll walk you through how to deploy to iOS in two easy steps."),
+ runnerSetupTitle: s__('Pipelines|1. Set up a runner'),
+ runnerSetupButton: s__('Pipelines|Set up a runner'),
+ runnerSetupBodyUnfinished: s__(
+ 'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline.',
+ ),
+ runnerSetupBodyFinished: s__(
+ 'Pipelines|You have runners available to run your job now. No need to do anything else.',
+ ),
+ runnerSetupPopoverTitle: s__(
+ "Pipelines|Let's get that runner set up! %{emojiStart}tada%{emojiEnd}",
+ ),
+ runnerSetupPopoverBodyLine1: s__(
+ 'Pipelines|Follow these instructions to install GitLab Runner on macOS.',
+ ),
+ runnerSetupPopoverBodyLine2: s__(
+ 'Pipelines|Need more information to set up your runner? %{linkStart}Check out our documentation%{linkEnd}.',
+ ),
+ configurePipelineTitle: s__('Pipelines|2. Configure deployment pipeline'),
+ configurePipelineBody: s__("Pipelines|We'll guide you through a simple pipeline set-up."),
+ configurePipelineButton: s__('Pipelines|Configure pipeline'),
+ noWalkthroughTitle: s__("Pipelines|Don't need a guide? Jump in right away with a template."),
+ noWalkthroughExplanation: s__('Pipelines|Based on your project, we recommend this template:'),
+ notBuildingForIos: s__(
+ "Pipelines|Not building for iOS or not what you're looking for? %{linkStart}See what else%{linkEnd} GitLab CI/CD has to offer.",
+ ),
+ },
+ data() {
+ return {
+ isModalShown: false,
+ isPopoverShown: false,
+ isRunnerSetupFinished: this.iosRunnersAvailable,
+ popoverTarget: `${this.$options.modalId}___BV_modal_content_`,
+ configurePipelineLink: mergeUrlParams(
+ { template: this.$options.iOSTemplateName },
+ this.pipelineEditorPath,
+ ),
+ };
+ },
+ computed: {
+ runnerSetupBodyText() {
+ return this.iosRunnersAvailable
+ ? this.$options.i18n.runnerSetupBodyFinished
+ : this.$options.i18n.runnerSetupBodyUnfinished;
+ },
+ },
+ methods: {
+ showModal() {
+ this.isModalShown = true;
+ },
+ hideModal() {
+ this.togglePopover();
+ this.isRunnerSetupFinished = true;
+ },
+ togglePopover() {
+ this.isPopoverShown = !this.isPopoverShown;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.i18n.title }}</h2>
+ <h3 class="gl-font-lg gl-text-gray-900 gl-mt-1">{{ $options.i18n.subtitle }}</h3>
+ <p>{{ $options.i18n.explanation }}</p>
+
+ <div class="gl-lg-display-flex">
+ <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
+ <gl-card body-class="gl-display-flex gl-flex-grow-1">
+ <div
+ class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
+ >
+ <div>
+ <div class="gl-py-5">
+ <gl-emoji
+ v-show="isRunnerSetupFinished"
+ class="gl-font-size-h2-xl"
+ data-name="white_check_mark"
+ data-testid="runner-setup-marked-completed"
+ />
+ <gl-emoji
+ v-show="!isRunnerSetupFinished"
+ class="gl-font-size-h2-xl"
+ data-name="tools"
+ data-testid="runner-setup-marked-todo"
+ />
+ </div>
+ <span class="gl-text-gray-800 gl-font-weight-bold">
+ {{ $options.i18n.runnerSetupTitle }}
+ </span>
+ <p class="gl-font-sm gl-mt-3">{{ runnerSetupBodyText }}</p>
+ </div>
+
+ <gl-button
+ v-if="!iosRunnersAvailable"
+ v-gl-modal-directive="$options.modalId"
+ category="primary"
+ variant="confirm"
+ @click="showModal"
+ >
+ {{ $options.i18n.runnerSetupButton }}
+ </gl-button>
+ <runner-instructions-modal
+ v-if="isModalShown"
+ :modal-id="$options.modalId"
+ :registration-token="registrationToken"
+ default-platform-name="osx"
+ @shown="togglePopover"
+ @hide="hideModal"
+ />
+ <gl-popover
+ v-if="isPopoverShown"
+ :show="true"
+ :show-close-button="true"
+ :target="popoverTarget"
+ triggers="manual"
+ placement="left"
+ fallback-placement="clockwise"
+ >
+ <template #title>
+ <gl-sprintf :message="$options.i18n.runnerSetupPopoverTitle">
+ <template #emoji="{ content }">
+ <gl-emoji class="gl-ml-2" :data-name="content" />
+ </template>
+ </gl-sprintf>
+ </template>
+ <div class="gl-mb-5">
+ {{ $options.i18n.runnerSetupPopoverBodyLine1 }}
+ </div>
+ <gl-sprintf :message="$options.i18n.runnerSetupPopoverBodyLine2">
+ <template #link="{ content }">
+ <gl-link :href="$options.runnerDocsLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-popover>
+ </div>
+ </gl-card>
+ </div>
+ <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
+ <gl-card body-class="gl-display-flex gl-flex-grow-1">
+ <div
+ class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
+ >
+ <div>
+ <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="tools" /></div>
+ <span class="gl-text-gray-800 gl-font-weight-bold">
+ {{ $options.i18n.configurePipelineTitle }}
+ </span>
+ <p class="gl-font-sm gl-mt-3">{{ $options.i18n.configurePipelineBody }}</p>
+ </div>
+
+ <gl-button
+ :disabled="!isRunnerSetupFinished"
+ category="primary"
+ variant="confirm"
+ data-testid="configure-pipeline-link"
+ :href="configurePipelineLink"
+ >
+ {{ $options.i18n.configurePipelineButton }}
+ </gl-button>
+ </div>
+ </gl-card>
+ </div>
+ </div>
+ <h3 class="gl-font-lg gl-text-gray-900 gl-mt-5">{{ $options.i18n.noWalkthroughTitle }}</h3>
+ <p>{{ $options.i18n.noWalkthroughExplanation }}</p>
+ <ci-templates
+ :filter-templates="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ $options.iOSTemplateName,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :disabled="!isRunnerSetupFinished"
+ />
+ <p>
+ <gl-sprintf :message="$options.i18n.notBuildingForIos">
+ <template #link="{ content }">
+ <gl-link :href="$options.whatElseLink">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
index be46a7f5cec..3eafb36bd1d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
@@ -33,19 +33,7 @@ export default {
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
I18N,
- inject: ['pipelineEditorPath'],
- props: {
- ciRunnerSettingsPath: {
- type: String,
- required: false,
- default: null,
- },
- anyRunnersAvailable: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
+ inject: ['anyRunnersAvailable', 'pipelineEditorPath', 'ciRunnerSettingsPath'],
data() {
return {
gettingStartedTemplateUrl: mergeUrlParams(
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
index 2b33467e948..e35fccf2d7e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue
@@ -36,7 +36,7 @@ export default {
};
</script>
<template>
- <div data-testid="pipeline-mini-graph">
+ <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle gl-my-1">
<div
v-for="stage in stages"
:key="stage.name"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
index afcb04cd7eb..53e21d4ce8b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
@@ -12,7 +12,8 @@
* 4. Commit widget
*/
-import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
@@ -21,7 +22,7 @@ import JobItem from './job_item.vue';
export default {
components: {
- GlIcon,
+ CiIcon,
GlLoadingIcon,
GlDropdown,
JobItem,
@@ -51,14 +52,6 @@ export default {
dropdownContent: [],
};
},
- computed: {
- triggerButtonClass() {
- return `ci-status-icon-${this.stage.status.group}`;
- },
- borderlessIcon() {
- return `${this.stage.status.icon}_borderless`;
- },
- },
watch: {
updateDropdown() {
if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) {
@@ -114,15 +107,21 @@ export default {
variant="link"
:aria-label="stageAriaLabel(stage.title)"
:lazy="true"
- :popper-opts="{ placement: 'bottom' }"
- :toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]"
+ :popper-opts="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ placement: 'bottom',
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :toggle-class="['gl-rounded-full!']"
menu-class="mini-pipeline-graph-dropdown-menu"
@show="onShowDropdown"
>
<template #button-content>
- <span class="gl-pointer-events-none">
- <gl-icon :name="borderlessIcon" />
- </span>
+ <ci-icon
+ is-interactive
+ css-classes="gl-rounded-full"
+ :size="24"
+ :status="stage.status"
+ class="gl-align-items-center gl-display-inline-flex"
+ />
</template>
<gl-loading-icon v-if="isLoading" size="sm" />
<ul
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index db9dc74863d..485e338f639 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -107,16 +107,11 @@ export default {
type: Object,
required: true,
},
- ciRunnerSettingsPath: {
+ registrationToken: {
type: String,
required: false,
default: null,
},
- anyRunnersAvailable: {
- type: Boolean,
- required: false,
- default: true,
- },
},
data() {
return {
@@ -386,8 +381,7 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
- :ci-runner-settings-path="ciRunnerSettingsPath"
- :any-runners-available="anyRunnersAvailable"
+ :registration-token="registrationToken"
/>
<gl-empty-state
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 77b9c2b5203..53da98434b0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -174,12 +174,13 @@ export default {
<div></div>
<linked-pipelines-mini-list
v-if="item.triggered_by"
- :triggered-by="[item.triggered_by]"
+ :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
+ item.triggered_by,
+ ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
data-testid="mini-graph-upstream"
/>
<pipeline-mini-graph
v-if="item.details && item.details.stages && item.details.stages.length > 0"
- class="gl-display-inline"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
@pipelineActionRequestComplete="onPipelineActionRequestComplete"
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 51373e712ff..9b0e6560c53 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -35,7 +35,7 @@ export default {
},
computed: {
...mapState(['pageInfo']),
- ...mapGetters(['getSuiteTests', 'getSuiteTestCount']),
+ ...mapGetters(['getSuiteTests', 'getSuiteTestCount', 'getSuiteArtifactsExpired']),
hasSuites() {
return this.getSuiteTests.length > 0;
},
@@ -80,7 +80,8 @@ export default {
<div
v-for="(testCase, index) in getSuiteTests"
:key="index"
- class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
+ class="gl-responsive-table-row rounded align-items-md-start"
+ data-testid="test-case-row"
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
@@ -157,7 +158,16 @@ export default {
</div>
<div v-else>
- <p class="js-no-test-cases">{{ s__('TestReports|There are no test cases to display.') }}</p>
+ <p data-testid="no-test-cases">
+ {{ s__('TestReports|There are no test cases to display.') }}
+ </p>
+ <p v-if="getSuiteArtifactsExpired" data-testid="artifacts-expired">
+ {{
+ s__(
+ 'TestReports|Test details are populated by job artifacts. The job artifacts from this pipeline are expired.',
+ )
+ }}
+ </p>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 36f708ff2af..0510992e962 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -9,6 +9,7 @@ export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
+export const PIPELINE_GRAPHQL_TYPE = 'Ci::Pipeline';
export const ICONS = {
TAG: 'tag',
@@ -44,6 +45,28 @@ export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';
+// Pipeline tabs
+
+export const TAB_QUERY_PARAM = 'tab';
+
+export const needsTabName = 'dag';
+export const jobsTabName = 'builds';
+export const failedJobsTabName = 'failures';
+export const testReportTabName = 'test_report';
+export const securityTabName = 'security';
+export const licensesTabName = 'licenses';
+export const codeQualityTabName = 'codequality_report';
+
+export const validPipelineTabNames = [
+ needsTabName,
+ jobsTabName,
+ failedJobsTabName,
+ testReportTabName,
+ securityTabName,
+ licensesTabName,
+ codeQualityTabName,
+];
+
// Constants for the ID and IID selection dropdown
export const PipelineKeyOptions = [
{
@@ -62,3 +85,27 @@ export const TOAST_MESSAGE = s__('Pipeline|Creating pipeline.');
export const BUTTON_TOOLTIP_RETRY = __('Retry failed jobs');
export const BUTTON_TOOLTIP_CANCEL = __('Cancel');
+
+export const DEFAULT_FIELDS = [
+ {
+ key: 'name',
+ label: __('Name'),
+ columnClass: 'gl-w-20p',
+ },
+ {
+ key: 'stage',
+ label: __('Stage'),
+ columnClass: 'gl-w-20p',
+ },
+ {
+ key: 'failure',
+ label: __('Failure'),
+ columnClass: 'gl-w-40p',
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'gl-text-right',
+ columnClass: 'gl-w-20p',
+ },
+];
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql b/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql
new file mode 100644
index 00000000000..1955cc9b0ac
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql
@@ -0,0 +1,12 @@
+mutation retryFailedJob($id: CiBuildID!) {
+ jobRetry(input: { id: $id }) {
+ job {
+ id
+ detailedStatus {
+ id
+ detailsPath
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipelines/graphql/provider.js b/app/assets/javascripts/pipelines/graphql/provider.js
new file mode 100644
index 00000000000..ef96b443da8
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/provider.js
@@ -0,0 +1,9 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+export default new VueApollo({
+ defaultClient: createDefaultClient(),
+});
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
new file mode 100644
index 00000000000..14e9a838f4b
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
@@ -0,0 +1,41 @@
+query getFailedJobs($fullPath: ID!, $pipelineIid: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ pipeline(iid: $pipelineIid) {
+ id
+ jobs(statuses: FAILED) {
+ nodes {
+ status
+ detailedStatus {
+ id
+ detailsPath
+ group
+ icon
+ label
+ text
+ tooltip
+ action {
+ id
+ buttonTitle
+ icon
+ method
+ path
+ title
+ }
+ }
+ id
+ stage {
+ id
+ name
+ }
+ name
+ retryable
+ userPermissions {
+ readBuild
+ updateBuild
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 338de65e795..fd869014570 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,10 +1,11 @@
import createFlash from '~/flash';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
import { createPipelineNotificationApp } from './pipeline_details_notification';
import { createPipelineJobsApp } from './pipeline_details_jobs';
+import { createPipelineFailedJobsApp } from './pipeline_details_failed_jobs';
import { apolloProvider } from './pipeline_shared_client';
import { createTestDetails } from './pipeline_test_details';
@@ -16,6 +17,7 @@ const SELECTORS = {
PIPELINE_TABS: '#js-pipeline-tabs',
PIPELINE_TESTS: '#js-pipeline-tests-detail',
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
+ PIPELINE_FAILED_JOBS: '#js-pipeline-failed-jobs-vue',
};
export default async function initPipelineDetailsBundle() {
@@ -79,5 +81,15 @@ export default async function initPipelineDetailsBundle() {
message: __('An error occurred while loading the Jobs tab.'),
});
}
+
+ if (gon.features?.failedJobsTabVue) {
+ try {
+ createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
+ } catch {
+ createFlash({
+ message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
+ });
+ }
+ }
}
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
index e2835ecc4d1..b2cb0457c4d 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_dag.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_dag.js
@@ -17,7 +17,7 @@ const createDagApp = (apolloProvider) => {
emptySvgPath,
pipelineProjectPath,
pipelineIid,
- } = el?.dataset;
+ } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
diff --git a/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js b/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
new file mode 100644
index 00000000000..7bf3b64bf47
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import FailedJobsApp from './components/jobs/failed_jobs_app.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const createPipelineFailedJobsApp = (selector) => {
+ const containerEl = document.querySelector(selector);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath, pipelineIid, failedJobsSummaryData } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ fullPath,
+ pipelineIid,
+ },
+ render(createElement) {
+ return createElement(FailedJobsApp, {
+ props: {
+ failedJobsSummary: JSON.parse(failedJobsSummaryData),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/pipelines/pipeline_details_header.js
index 1c619768764..2fedd7e7a98 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_header.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_header.js
@@ -11,7 +11,7 @@ export const createPipelineHeaderApp = (elSelector, apolloProvider, graphqlResou
return;
}
- const { fullPath, pipelineId, pipelineIid, pipelinesPath } = el?.dataset;
+ const { fullPath, pipelineId, pipelineIid, pipelinesPath } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
diff --git a/app/assets/javascripts/pipelines/pipeline_details_notification.js b/app/assets/javascripts/pipelines/pipeline_details_notification.js
index 0061be843c5..b480fc7c713 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_notification.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_notification.js
@@ -11,7 +11,7 @@ export const createPipelineNotificationApp = (elSelector, apolloProvider) => {
return;
}
- const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el?.dataset;
+ const { deprecatedKeywordsDocPath, fullPath, pipelineIid } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js
index ff88c6215e5..530917f0402 100644
--- a/app/assets/javascripts/pipelines/pipeline_tabs.js
+++ b/app/assets/javascripts/pipelines/pipeline_tabs.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
-import { reportToSentry } from './utils';
+import { removeParams, updateHistory } from '~/lib/utils/url_utility';
+import { TAB_QUERY_PARAM } from '~/pipelines/constants';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getPipelineDefaultTab, reportToSentry } from './utils';
Vue.use(VueApollo);
@@ -17,7 +20,19 @@ const createPipelineTabs = (selector, apolloProvider) => {
downloadablePathForReportType,
exposeSecurityDashboard,
exposeLicenseScanningData,
+ graphqlResourceEtag,
+ pipelineIid,
+ pipelineProjectPath,
} = dataset;
+
+ const defaultTabValue = getPipelineDefaultTab(window.location.href);
+
+ updateHistory({
+ url: removeParams([TAB_QUERY_PARAM]),
+ title: document.title,
+ replace: true,
+ });
+
// eslint-disable-next-line no-new
new Vue({
el: selector,
@@ -26,11 +41,15 @@ const createPipelineTabs = (selector, apolloProvider) => {
},
apolloProvider,
provide: {
- canGenerateCodequalityReports: JSON.parse(canGenerateCodequalityReports),
+ canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports),
codequalityReportDownloadPath,
+ defaultTabValue,
downloadablePathForReportType,
- exposeSecurityDashboard: JSON.parse(exposeSecurityDashboard),
- exposeLicenseScanningData: JSON.parse(exposeLicenseScanningData),
+ exposeSecurityDashboard: parseBoolean(exposeSecurityDashboard),
+ exposeLicenseScanningData: parseBoolean(exposeLicenseScanningData),
+ graphqlResourceEtag,
+ pipelineIid,
+ pipelineProjectPath,
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_tabs', `error: ${err}, info: ${info}`);
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js
index f4d9a44a754..6dccdb1a3e6 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/pipelines/pipelines_index.js
@@ -40,6 +40,8 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
params,
ciRunnerSettingsPath,
anyRunnersAvailable,
+ iosRunnersAvailable,
+ registrationToken,
} = el.dataset;
return new Vue({
@@ -49,6 +51,9 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
artifactsEndpoint,
artifactsEndpointPlaceholder,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
+ ciRunnerSettingsPath,
+ anyRunnersAvailable: parseBoolean(anyRunnersAvailable),
+ iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
},
data() {
return {
@@ -78,8 +83,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
projectId,
defaultBranchName,
params: JSON.parse(params),
- ciRunnerSettingsPath,
- anyRunnersAvailable: parseBoolean(anyRunnersAvailable),
+ registrationToken,
},
});
},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index b7f590a7b3c..f0556f3d12e 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -38,11 +38,7 @@ export const fetchTestSuite = ({ state, commit, dispatch }, index) => {
return axios
.get(state.suiteEndpoint, { params: { build_ids } })
.then(({ data }) => commit(types.SET_SUITE, { suite: data, index }))
- .catch(() => {
- createFlash({
- message: s__('TestReports|There was an error fetching the test suite.'),
- });
- })
+ .catch((error) => commit(types.SET_SUITE_ERROR, error))
.finally(() => {
dispatch('toggleLoading');
});
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/constants.js b/app/assets/javascripts/pipelines/stores/test_reports/constants.js
new file mode 100644
index 00000000000..8eebfb6b208
--- /dev/null
+++ b/app/assets/javascripts/pipelines/stores/test_reports/constants.js
@@ -0,0 +1 @@
+export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts have expired';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
index 03680de0fa9..e6a88bb4175 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/getters.js
@@ -1,4 +1,5 @@
import { addIconStatus, formatFilePath, formattedTime } from './utils';
+import { ARTIFACTS_EXPIRED_ERROR_MESSAGE } from './constants';
export const getTestSuites = (state) => {
const { test_suites: testSuites = [] } = state.testReports;
@@ -29,3 +30,6 @@ export const getSuiteTests = (state) => {
};
export const getSuiteTestCount = (state) => getSelectedSuite(state)?.test_cases?.length || 0;
+
+export const getSuiteArtifactsExpired = (state) =>
+ state.errorMessage === ARTIFACTS_EXPIRED_ERROR_MESSAGE;
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
index 803f6bf60b1..7651a2f4327 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
@@ -2,4 +2,5 @@ export const SET_PAGE = 'SET_PAGE';
export const SET_SELECTED_SUITE_INDEX = 'SET_SELECTED_SUITE_INDEX';
export const SET_SUMMARY = 'SET_SUMMARY';
export const SET_SUITE = 'SET_SUITE';
+export const SET_SUITE_ERROR = 'SET_SUITE_ERROR';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index cf0bf8483dd..68ee063dda7 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,3 +1,5 @@
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
import * as types from './mutation_types';
export default {
@@ -13,6 +15,18 @@ export default {
state.testReports.test_suites[index] = { ...suite, hasFullSuite: true };
},
+ [types.SET_SUITE_ERROR](state, error) {
+ const errorMessage = error.response?.data?.errors;
+
+ if (errorMessage) {
+ state.errorMessage = errorMessage;
+ } else {
+ createFlash({
+ message: s__('TestReports|There was an error fetching the test suite.'),
+ });
+ }
+ },
+
[types.SET_SELECTED_SUITE_INDEX](state, selectedSuiteIndex) {
Object.assign(state, { selectedSuiteIndex });
},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
index 0ee6f53fa58..3ec9418c14e 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -5,6 +5,7 @@ export default ({ blobPath = '', summaryEndpoint = '', suiteEndpoint = '' }) =>
testReports: {},
selectedSuiteIndex: null,
isLoading: false,
+ errorMessage: null,
pageInfo: {
page: 1,
perPage: 20,
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index f6e1c8b7412..588d15495ab 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,7 +1,12 @@
import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash';
-import { SUPPORTED_FILTER_PARAMETERS, NEEDS_PROPERTY } from './constants';
-
+import { getParameterValues } from '~/lib/utils/url_utility';
+import {
+ NEEDS_PROPERTY,
+ SUPPORTED_FILTER_PARAMETERS,
+ TAB_QUERY_PARAM,
+ validPipelineTabNames,
+} from './constants';
/*
The following functions are the main engine in transforming the data as
received from the endpoint into the format the d3 graph expects.
@@ -138,3 +143,13 @@ export const reportMessageToSentry = (component, message, context) => {
Sentry.captureMessage(message);
});
};
+
+export const getPipelineDefaultTab = (url) => {
+ const [tabQueryValue] = getParameterValues(TAB_QUERY_PARAM, url);
+
+ if (tabQueryValue && validPipelineTabNames.includes(tabQueryValue)) {
+ return tabQueryValue;
+ }
+
+ return null;
+};