Skip to content
Snippets Groups Projects
Commit 412f7af0 authored by Mireya Gen Andres's avatar Mireya Gen Andres Committed by Matthias Käppler
Browse files

Filter duplicated downstreams in mini pipeline graph using REST

This updates the mini pipeline graph for the MR widget and the
pipelines table.

Changelog: fixed
parent 81339b88
No related branches found
No related tags found
No related merge requests found
Showing
with 181 additions and 12 deletions
Loading
Loading
@@ -171,11 +171,12 @@ export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipe
 
export const generateColumnsFromLayersListMemoized = memoize(generateColumnsFromLayersListBare);
 
// TODO: handle REST / MR values
// See https://gitlab.com/gitlab-org/gitlab/-/issues/367547
export const keepLatestDownstreamPipelines = (downstreamPipelines = []) => {
// handles GraphQL
return downstreamPipelines.filter((pipeline) => {
if (pipeline.source_job) {
return !pipeline?.source_job?.retried || false;
}
return !pipeline?.sourceJob?.retried || false;
});
};
Loading
Loading
@@ -2,6 +2,7 @@
import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
Loading
Loading
@@ -115,6 +116,10 @@ export default {
eventHub.$off('openConfirmationModal', this.setModalData);
},
methods: {
getDownstreamPipelines(pipeline) {
const downstream = pipeline.triggered;
return keepLatestDownstreamPipelines(downstream);
},
setModalData(data) {
this.pipelineId = data.pipeline.id;
this.pipeline = data.pipeline;
Loading
Loading
@@ -171,7 +176,7 @@ export default {
 
<template #cell(stages)="{ item }">
<pipeline-mini-graph
:downstream-pipelines="item.triggered"
:downstream-pipelines="getDownstreamPipelines(item)"
:pipeline-path="item.path"
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
Loading
Loading
Loading
Loading
@@ -11,6 +11,7 @@ import {
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
Loading
Loading
@@ -86,6 +87,10 @@ export default {
},
},
computed: {
downstreamPipelines() {
const downstream = this.pipeline.triggered;
return keepLatestDownstreamPipelines(downstream);
},
hasPipeline() {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
Loading
Loading
@@ -274,7 +279,7 @@ export default {
<span class="gl-align-items-center gl-display-inline-flex">
<pipeline-mini-graph
v-if="pipeline.details.stages"
:downstream-pipelines="pipeline.triggered"
:downstream-pipelines="downstreamPipelines"
:is-merge-train="isMergeTrain"
:pipeline-path="pipeline.path"
:stages="pipeline.details.stages"
Loading
Loading
Loading
Loading
@@ -15,6 +15,9 @@ class TriggeredPipelineEntity < Grape::Entity
expose :name do |pipeline|
pipeline.source_job&.name
end
expose :retried do |pipeline|
pipeline.source_job&.retried
end
end
 
expose :path do |pipeline|
Loading
Loading
const downstream = {
export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({
nodes: [
{
id: 'gid://gitlab/Ci::Pipeline/612',
Loading
Loading
@@ -17,7 +17,7 @@ const downstream = {
},
sourceJob: {
id: 'gid://gitlab/Ci::Bridge/532',
retried: false,
retried: includeSourceJobRetried ? false : null,
},
__typename: 'Pipeline',
},
Loading
Loading
@@ -38,7 +38,7 @@ const downstream = {
},
sourceJob: {
id: 'gid://gitlab/Ci::Bridge/531',
retried: true,
retried: includeSourceJobRetried ? true : null,
},
__typename: 'Pipeline',
},
Loading
Loading
@@ -59,13 +59,13 @@ const downstream = {
},
sourceJob: {
id: 'gid://gitlab/Ci::Bridge/530',
retried: true,
retried: includeSourceJobRetried ? true : null,
},
__typename: 'Pipeline',
},
],
__typename: 'PipelineConnection',
};
});
 
const upstream = {
id: 'gid://gitlab/Ci::Pipeline/610',
Loading
Loading
@@ -161,7 +161,7 @@ export const mockDownstreamQueryResponse = {
pipeline: {
path: '/root/ci-project/-/pipelines/790',
id: 'pipeline-1',
downstream,
downstream: mockDownstreamPipelinesGraphql(),
upstream: null,
},
__typename: 'Project',
Loading
Loading
@@ -176,7 +176,7 @@ export const mockUpstreamDownstreamQueryResponse = {
pipeline: {
id: 'pipeline-1',
path: '/root/ci-project/-/pipelines/790',
downstream,
downstream: mockDownstreamPipelinesGraphql(),
upstream,
},
__typename: 'Project',
Loading
Loading
Loading
Loading
@@ -23,8 +23,19 @@
let!(:build_test) { create(:ci_build, pipeline: pipeline, stage: 'test') }
let!(:build_deploy_failed) { create(:ci_build, status: :failed, pipeline: pipeline, stage: 'deploy') }
 
let(:bridge) { create(:ci_bridge, pipeline: pipeline) }
let(:retried_bridge) { create(:ci_bridge, :retried, pipeline: pipeline) }
let(:downstream_pipeline) { create(:ci_pipeline, :with_job) }
let(:retried_downstream_pipeline) { create(:ci_pipeline, :with_job) }
let!(:ci_sources_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
let!(:retried_ci_sources_pipeline) do
create(:ci_sources_pipeline, pipeline: retried_downstream_pipeline, source_job: retried_bridge)
end
before do
sign_in(user)
project.add_developer(user)
end
 
it 'pipelines/pipelines.json' do
Loading
Loading
Loading
Loading
@@ -121,6 +121,14 @@ describe('Pipelines Table', () => {
expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength);
});
 
it('should render the latest downstream pipelines only', () => {
// component receives two downstream pipelines. one of them is already outdated
// because we retried the trigger job, so the mini pipeline graph will only
// render the newly created downstream pipeline instead
expect(pipeline.triggered).toHaveLength(2);
expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();
Loading
Loading
Loading
Loading
@@ -3,6 +3,7 @@ import {
makeLinksFromNodes,
filterByAncestors,
generateColumnsFromLayersListBare,
keepLatestDownstreamPipelines,
listByLayers,
parseData,
removeOrphanNodes,
Loading
Loading
@@ -10,6 +11,8 @@ import {
} from '~/pipelines/components/parsing_utils';
import { createNodeDict } from '~/pipelines/utils';
 
import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data';
import { mockDownstreamPipelinesGraphql } from '../commit/mock_data';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
 
Loading
Loading
@@ -159,3 +162,37 @@ describe('DAG visualization parsing utilities', () => {
});
});
});
describe('linked pipeline utilities', () => {
describe('keepLatestDownstreamPipelines', () => {
it('filters data from GraphQL', () => {
const downstream = mockDownstreamPipelinesGraphql().nodes;
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(downstream).toHaveLength(3);
expect(latestDownstream).toHaveLength(1);
});
it('filters data from REST', () => {
const downstream = mockDownstreamPipelinesRest();
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(downstream).toHaveLength(2);
expect(latestDownstream).toHaveLength(1);
});
it('returns downstream pipelines if sourceJob.retried is null', () => {
const downstream = mockDownstreamPipelinesGraphql({ includeSourceJobRetried: false }).nodes;
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(latestDownstream).toHaveLength(downstream.length);
});
it('returns downstream pipelines if source_job.retried is null', () => {
const downstream = mockDownstreamPipelinesRest({ includeSourceJobRetried: false });
const latestDownstream = keepLatestDownstreamPipelines(downstream);
expect(latestDownstream).toHaveLength(downstream.length);
});
});
});
Loading
Loading
@@ -111,6 +111,14 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
 
it('should render the latest downstream pipelines only', () => {
// component receives two downstream pipelines. one of them is already outdated
// because we retried the trigger job, so the mini pipeline graph will only
// render the newly created downstream pipeline instead
expect(mockData.pipeline.triggered).toHaveLength(2);
expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
});
describe('should render pipeline coverage information', () => {
it('should render coverage percentage', () => {
expect(findPipelineCoverage().text()).toMatch(
Loading
Loading
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
 
export const mockDownstreamPipelinesRest = ({ includeSourceJobRetried = true } = {}) => [
{
id: 632,
user: {
id: 1,
username: 'root',
name: 'Administrator',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'https://gdk.test:3000/root',
show_status: false,
path: '/root',
},
active: false,
coverage: null,
source: 'parent_pipeline',
source_job: {
name: 'bridge_job',
retried: includeSourceJobRetried ? false : null,
},
path: '/kitchen-sink/bakery/-/pipelines/632',
details: {
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/kitchen-sink/bakery/-/pipelines/632',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
},
project: {
id: 21,
name: 'bakery',
full_path: '/kitchen-sink/bakery',
full_name: 'kitchen-sink / bakery',
refs_url: '/kitchen-sink/bakery/refs',
},
},
{
id: 633,
user: {
id: 1,
username: 'root',
name: 'Administrator',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
web_url: 'https://gdk.test:3000/root',
show_status: false,
path: '/root',
},
active: false,
coverage: null,
source: 'parent_pipeline',
source_job: {
name: 'bridge_job',
retried: includeSourceJobRetried ? true : null,
},
path: '/kitchen-sink/bakery/-/pipelines/633',
details: {
status: {
icon: 'status_success',
text: 'passed',
label: 'passed',
group: 'success',
tooltip: 'passed',
has_details: true,
details_path: '/kitchen-sink/bakery/-/pipelines/633',
illustration: null,
favicon:
'/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
},
},
project: {
id: 21,
name: 'bakery',
full_path: '/kitchen-sink/bakery',
full_name: 'kitchen-sink / bakery',
refs_url: '/kitchen-sink/bakery/refs',
},
},
];
export const artifacts = [
{
text: 'result.txt',
Loading
Loading
@@ -207,6 +296,7 @@ export default {
retry_path: '/root/acets-app/pipelines/172/retry',
created_at: '2017-04-07T12:27:19.520Z',
updated_at: '2017-04-07T15:28:44.800Z',
triggered: mockDownstreamPipelinesRest(),
},
pipelineCoverageDelta: '15.25',
buildsWithCoverage: [
Loading
Loading
Loading
Loading
@@ -182,6 +182,7 @@
 
expect(source_jobs[cross_project_pipeline.id][:name]).to eq('cross-project')
expect(source_jobs[child_pipeline.id][:name]).to eq('child')
expect(source_jobs[child_pipeline.id][:retried]).to eq false
end
end
end
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment