{{ columnTitle }}
@@ -206,8 +218,12 @@ export default {
+import { GlCard } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
export default {
+ components: {
+ GlCard,
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
stageClasses: {
type: String,
@@ -12,18 +19,37 @@ export default {
default: '',
},
},
+ computed: {
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ },
};
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index 6030adc96ad9baac23832b741e503970481c565b..e144b9aab0cbad5fc1ac9018157642f8d4a9a834 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -68,7 +68,7 @@ export default {
required: true,
},
},
- jobClasses: [
+ legacyJobClasses: [
'gl-p-3',
'gl-border-gray-100',
'gl-border-solid',
@@ -82,18 +82,43 @@ export default {
'gl-hover-border-gray-200',
'gl-focus-border-gray-200',
],
- titleClasses: [
+ jobClasses: [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ],
+ legacyTitleClasses: [
'gl-font-weight-bold',
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
'gl-pl-3',
],
+ titleClasses: [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ],
computed: {
canUpdatePipeline() {
return this.userPermissions.updatePipeline;
},
columnSpacingClass() {
+ if (this.isNewPipelineGraph) {
+ const baseClasses = 'stage-column gl-relative gl-flex-basis-full';
+ return this.isStageView
+ ? `${baseClasses} is-stage-view gl-m-5`
+ : `${baseClasses} gl-my-5 gl-mx-7`;
+ }
+
return this.isStageView ? 'gl-px-6' : 'gl-px-9';
},
hasAction() {
@@ -102,6 +127,17 @@ export default {
showStageName() {
return !this.isStageView;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ jobClasses() {
+ return this.isNewPipelineGraph ? this.$options.jobClasses : this.$options.legacyJobClasses;
+ },
+ titleClasses() {
+ return this.isNewPipelineGraph
+ ? this.$options.titleClasses
+ : this.$options.legacyTitleClasses;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
@@ -135,12 +171,16 @@ export default {
};
-
+
{{ name }}
@@ -161,7 +201,11 @@ export default {
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
- class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
+ class="gl-relative gl-white-space-normal gl-pipeline-job-width"
+ :class="{
+ 'gl-mb-3': !isNewPipelineGraph,
+ 'gl-mb-2': isNewPipelineGraph,
+ }"
@mouseenter="$emit('jobHover', group.name)"
@mouseleave="$emit('jobHover', '')"
>
@@ -174,7 +218,7 @@ export default {
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
:class="[
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
@@ -188,7 +232,7 @@ export default {
:group="group"
:stage-name="showStageName ? group.stageName : ''"
:pipeline-id="pipelineId"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
/>
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 31d45ad3a28559f632bed89ec25e3ab738083be7..9a1faf271438d0b90ffb5b9be39da9dad4d7140f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -811,7 +811,7 @@ $ci-action-icon-size: 22px;
$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
$ci-action-dropdown-button-size: 24px;
-$ci-action-dropdown-svg-size: 12px;
+$ci-action-dropdown-svg-size: 16px;
/*
CI variable lists
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 98e9e2b3c27f57c8ce1cb2a2cdb1bd294f8bd062..dcd8f90ab1c91d3efb85fcc4603b3794d8f0564d 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -125,21 +125,27 @@
// They are here to still access a variable or because they use magic values.
// scoped to the graph. Do not add other styles.
.gl-pipeline-min-h {
- min-height: $dropdown-max-height-lg;
+ min-height: calc(#{$dropdown-max-height-lg} + #{$gl-spacing-scale-6});
}
.gl-pipeline-job-width {
width: 100%;
- max-width: 400px;
}
.gl-pipeline-job-width\! {
width: 100% !important;
- max-width: 400px !important;
}
.gl-downstream-pipeline-job-width {
width: 8rem;
+
+ .pipeline-graph-container & {
+ width: 100%;
+
+ @media (min-width: $breakpoint-sm) {
+ width: 8rem;
+ }
+ }
}
.gl-linked-pipeline-padding {
@@ -154,8 +160,8 @@
// Action Icons in big pipeline-graph nodes
&.ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
+ height: 24px;
+ width: 24px;
border-radius: 100%;
display: block;
padding: 0;
@@ -242,3 +248,64 @@
}
}
}
+
+.pipeline-graph-container {
+ .stage-column.is-stage-view:not(:last-of-type)::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: $gl-spacing-scale-6;
+ width: 2px;
+ height: $gl-spacing-scale-5 * 2;
+ background-color: $gray-200;
+
+ @media (min-width: $breakpoint-sm) {
+ top: 1.25rem;
+ left: 100%;
+ width: $gl-spacing-scale-5 * 2;
+ height: 2px;
+ }
+ }
+
+ .stage-column,
+ .stage-column.is-stage-view {
+ @media (min-width: $breakpoint-sm) {
+ &:first-of-type {
+ margin-left: $gl-spacing-scale-6;
+ }
+ }
+ }
+
+ .linked-pipeline-container[aria-expanded=true] {
+ @media (max-width: $breakpoint-sm) {
+ width: 100%;
+
+ > div {
+ border-bottom-left-radius: 0;
+ }
+
+ > div > button {
+ border-bottom-right-radius: 0 !important;
+ }
+ }
+ }
+
+ .linked-pipelines-column,
+ .pipeline-show-container,
+ .pipeline-links-container {
+ @media (max-width: $breakpoint-sm) {
+ width: 100%;
+ }
+ }
+
+ .pipeline-graph {
+ @media (max-width: $breakpoint-sm) {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+ }
+
+ .pipeline-graph .pipeline-graph {
+ background-color: $gray-100;
+ }
+}
diff --git a/config/feature_flags/development/new_pipeline_graph.yml b/config/feature_flags/development/new_pipeline_graph.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d3570980f63a35db71300dc7e98a56b9fdd8efd5
--- /dev/null
+++ b/config/feature_flags/development/new_pipeline_graph.yml
@@ -0,0 +1,8 @@
+---
+name: new_pipeline_graph
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132462
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426902
+milestone: '16.5'
+type: development
+group: group::ux paper cuts
+default_enabled: false
diff --git a/ee/app/controllers/ee/projects/pipelines_controller.rb b/ee/app/controllers/ee/projects/pipelines_controller.rb
index ee996f4de65b2aa31b07ba558a1a72978ba5fe0f..c0c8df82fffebffbde02971ab231f74b174d0234 100644
--- a/ee/app/controllers/ee/projects/pipelines_controller.rb
+++ b/ee/app/controllers/ee/projects/pipelines_controller.rb
@@ -14,6 +14,7 @@ module PipelinesController
push_frontend_feature_flag(:pipeline_security_dashboard_graphql, project, type: :development)
push_frontend_feature_flag(:dora_charts_forecast, project.namespace)
push_frontend_feature_flag(:use_holt_winters_forecast_for_deployment_frequency, project)
+ push_frontend_feature_flag(:new_pipeline_graph, project, type: :development)
end
feature_category :software_composition_analysis, [:licenses]
diff --git a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
index a98e79c69fe123d0279ac5cc1544b46cd62f125e..c3f22749978c66588d6be2ae90af7f8554b34680 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/graph_component_spec.js
@@ -19,6 +19,10 @@ describe('graph component', () => {
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent);
const findStageNameInJob = () => wrapper.findByTestId('stage-name-in-job');
+ const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
+ const findRootGraphLayout = () => wrapper.findByTestId('stage-column');
+ const findStageColumnTitle = () => wrapper.findByTestId('stage-column-title');
+ const findJobItem = () => wrapper.findComponent(JobItem);
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
@@ -42,6 +46,9 @@ describe('graph component', () => {
mountFn = shallowMount,
props = {},
stubOverride = {},
+ glFeatures = {
+ newPipelineGraph: false,
+ },
} = {}) => {
wrapper = mountFn(PipelineGraph, {
propsData: {
@@ -61,6 +68,9 @@ describe('graph component', () => {
'job-group-dropdown': true,
...stubOverride,
},
+ provide: {
+ glFeatures,
+ },
});
};
@@ -112,9 +122,8 @@ describe('graph component', () => {
});
it('dims unrelated jobs', () => {
- const unrelatedJob = wrapper.findComponent(JobItem);
expect(findLinksLayer().emitted().highlightedJobsChange).toHaveLength(1);
- expect(unrelatedJob.classes('gl-opacity-3')).toBe(true);
+ expect(findJobItem().classes('gl-opacity-3')).toBe(true);
});
});
});
@@ -179,4 +188,82 @@ describe('graph component', () => {
expect(findDownstreamColumn().props().linkedPipelines).toHaveLength(1);
});
});
+
+ describe.each`
+ name | value | state
+ ${'disabled'} | ${false} | ${'should not'}
+ ${'enabled'} | ${true} | ${'should'}
+ `('With feature flag newPipelineGraph $name', ({ value, state }) => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ stubOverride: { 'job-item': false, StageColumnComponent },
+ glFeatures: {
+ newPipelineGraph: value,
+ },
+ stubs: {
+ StageColumnComponent,
+ },
+ });
+ });
+
+ it(`${state} add class pipeline-graph-container on wrapper`, () => {
+ expect(findPipelineContainer().classes('pipeline-graph-container')).toBe(value);
+ });
+
+ it(`${state} add class is-stage-view on rootGraphLayout`, () => {
+ expect(findRootGraphLayout().classes('is-stage-view')).toBe(value);
+ });
+
+ it(`${state} add titleClasses on stageColumnTitle`, () => {
+ const titleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ];
+ const legacyTitleClasses = [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-3',
+ ];
+ const checkClasses = value ? titleClasses : legacyTitleClasses;
+
+ expect(findStageColumnTitle().classes()).toEqual(expect.arrayContaining(checkClasses));
+ });
+
+ it(`${state} add jobClasses on findJobItem`, () => {
+ const jobClasses = [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ];
+ const legacyJobClasses = [
+ 'gl-p-3',
+ 'gl-border-gray-100',
+ 'gl-border-solid',
+ 'gl-border-1',
+ 'gl-bg-white',
+ 'gl-rounded-7',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ 'gl-hover-border-gray-200',
+ 'gl-focus-border-gray-200',
+ ];
+ const checkClasses = value ? jobClasses : legacyJobClasses;
+
+ expect(findJobItem().props('cssClassJobName')).toEqual(expect.arrayContaining(checkClasses));
+ });
+ });
});