Commit 211f3dbe authored by Miranda Fluharty's avatar Miranda Fluharty Committed by Kushal Pandya

Extend pipeline graph scroll area to full width

Use debounced resize function to set left padding inside graph
Set padding such that the left edge of the pipeline lines up with
the fixed-width container above it in the default scroll position
Also trigger padding recalculation on layout change
parent b0a3500c
...@@ -2,35 +2,43 @@ ...@@ -2,35 +2,43 @@
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import StageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
import GraphMixin from '../../mixins/graph_component_mixin'; import GraphMixin from '../../mixins/graph_component_mixin';
import GraphWidthMixin from '~/pipelines/mixins/graph_width_mixin';
export default { export default {
components: { components: {
StageColumnComponent, StageColumnComponent,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [GraphMixin], mixins: [GraphMixin, GraphWidthMixin],
}; };
</script> </script>
<template> <template>
<div class="build-content middle-block js-pipeline-graph"> <div class="build-content middle-block js-pipeline-graph">
<div class="pipeline-visualization pipeline-graph pipeline-tab-content"> <div class="pipeline-visualization pipeline-graph pipeline-tab-content">
<div v-if="isLoading" class="m-auto"><gl-loading-icon :size="3" /></div> <div
:style="{
paddingLeft: `${graphLeftPadding}px`,
paddingRight: `${graphRightPadding}px`,
}"
>
<gl-loading-icon v-if="isLoading" class="m-auto" :size="3" />
<ul v-if="!isLoading" class="stage-column-list"> <ul v-if="!isLoading" class="stage-column-list">
<stage-column-component <stage-column-component
v-for="(stage, index) in graph" v-for="(stage, index) in graph"
:key="stage.name" :key="stage.name"
:class="{ :class="{
'append-right-48': shouldAddRightMargin(index), 'append-right-48': shouldAddRightMargin(index),
}" }"
:title="capitalizeStageName(stage.name)" :title="capitalizeStageName(stage.name)"
:groups="stage.groups" :groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action="stage.status.action" :action="stage.status.action"
@refreshPipelineGraph="refreshPipelineGraph" @refreshPipelineGraph="refreshPipelineGraph"
/> />
</ul> </ul>
</div>
</div> </div>
</div> </div>
</template> </template>
export const CANCEL_REQUEST = 'CANCEL_REQUEST'; export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const PIPELINES_TABLE = 'PIPELINES_TABLE'; export const PIPELINES_TABLE = 'PIPELINES_TABLE';
export const LAYOUT_CHANGE_DELAY = 300;
import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
import { LAYOUT_CHANGE_DELAY } from '~/pipelines/constants';
export default {
debouncedResize: null,
sidebarMutationObserver: null,
data() {
return {
graphLeftPadding: 0,
graphRightPadding: 0,
};
},
beforeDestroy() {
window.removeEventListener('resize', this.$options.debouncedResize);
if (this.$options.sidebarMutationObserver) {
this.$options.sidebarMutationObserver.disconnect();
}
},
created() {
this.$options.debouncedResize = debounceByAnimationFrame(this.setGraphPadding);
window.addEventListener('resize', this.$options.debouncedResize);
},
mounted() {
this.setGraphPadding();
this.$options.sidebarMutationObserver = new MutationObserver(this.handleLayoutChange);
this.$options.sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
attributes: true,
childList: false,
subtree: false,
});
},
methods: {
setGraphPadding() {
// only add padding to main graph (not inline upstream/downstream graphs)
if (this.type && this.type !== 'main') return;
const container = document.querySelector('.js-pipeline-container');
if (!container) return;
this.graphLeftPadding = container.offsetLeft;
this.graphRightPadding = window.innerWidth - container.offsetLeft - container.offsetWidth;
},
handleLayoutChange() {
// wait until animations finish, then recalculate padding
window.setTimeout(this.setGraphPadding, LAYOUT_CHANGE_DELAY);
},
},
};
...@@ -453,7 +453,7 @@ ...@@ -453,7 +453,7 @@
display: flex; display: flex;
width: 100%; width: 100%;
background-color: $gray-light; background-color: $gray-light;
padding: $gl-padding; padding: $gl-padding 0;
overflow: auto; overflow: auto;
} }
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project = render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content .tab-content
#js-tab-pipeline.tab-pane #js-tab-pipeline.tab-pane.position-absolute.position-left-0.w-100
#js-pipeline-graph-vue #js-pipeline-graph-vue
#js-tab-builds.tab-pane #js-tab-builds.tab-pane
......
---
title: Extend pipeline graph scroll area to full width
merge_request: 14870
author:
type: changed
...@@ -3,6 +3,7 @@ import _ from 'underscore'; ...@@ -3,6 +3,7 @@ import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
import GraphMixin from '~/pipelines/mixins/graph_component_mixin'; import GraphMixin from '~/pipelines/mixins/graph_component_mixin';
import GraphWidthMixin from '~/pipelines/mixins/graph_width_mixin';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue';
import GraphEEMixin from 'ee/pipelines/mixins/graph_pipeline_bundle_mixin'; import GraphEEMixin from 'ee/pipelines/mixins/graph_pipeline_bundle_mixin';
...@@ -13,7 +14,7 @@ export default { ...@@ -13,7 +14,7 @@ export default {
GlLoadingIcon, GlLoadingIcon,
LinkedPipelinesColumn, LinkedPipelinesColumn,
}, },
mixins: [GraphMixin, GraphEEMixin], mixins: [GraphMixin, GraphWidthMixin, GraphEEMixin],
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -102,82 +103,89 @@ export default { ...@@ -102,82 +103,89 @@ export default {
class="pipeline-visualization pipeline-graph" class="pipeline-visualization pipeline-graph"
:class="{ 'pipeline-tab-content': !isLinkedPipeline }" :class="{ 'pipeline-tab-content': !isLinkedPipeline }"
> >
<div v-if="isLoading" class="m-auto"><gl-loading-icon :size="3" /></div> <div
:style="{
paddingLeft: `${graphLeftPadding}px`,
paddingRight: `${graphRightPadding}px`,
}"
>
<gl-loading-icon v-if="isLoading" class="m-auto" :size="3" />
<pipeline-graph <pipeline-graph
v-if="type !== $options.downstream && expandedTriggeredBy" v-if="type !== $options.downstream && expandedTriggeredBy"
type="upstream" type="upstream"
class="d-inline-block upstream-pipeline" class="d-inline-block upstream-pipeline"
:class="`js-upstream-pipeline-${expandedTriggeredBy.id}`" :class="`js-upstream-pipeline-${expandedTriggeredBy.id}`"
:is-loading="false" :is-loading="false"
:pipeline="expandedTriggeredBy" :pipeline="expandedTriggeredBy"
:is-linked-pipeline="true" :is-linked-pipeline="true"
:mediator="mediator" :mediator="mediator"
@onClickTriggeredBy=" @onClickTriggeredBy="
(parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline) (parentPipeline, pipeline) => clickTriggeredByPipeline(parentPipeline, pipeline)
" "
@refreshPipelineGraph="requestRefreshPipelineGraph" @refreshPipelineGraph="requestRefreshPipelineGraph"
/> />
<linked-pipelines-column <linked-pipelines-column
v-if="hasTriggeredBy" v-if="hasTriggeredBy"
:linked-pipelines="triggeredByPipelines" :linked-pipelines="triggeredByPipelines"
:column-title="__('Upstream')" :column-title="__('Upstream')"
graph-position="left" graph-position="left"
@linkedPipelineClick=" @linkedPipelineClick="
linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline) linkedPipeline => $emit('onClickTriggeredBy', pipeline, linkedPipeline)
" "
/> />
<ul <ul
v-if="!isLoading" v-if="!isLoading"
:class="{
'inline js-has-linked-pipelines': hasTriggered || hasTriggeredBy,
}"
class="stage-column-list align-top"
>
<stage-column-component
v-for="(stage, index) in graph"
:key="stage.name"
:class="{ :class="{
'has-upstream prepend-left-64': index === 0 && hasTriggeredBy, 'inline js-has-linked-pipelines': hasTriggered || hasTriggeredBy,
'has-downstream': index === graph.length - 1 && hasTriggered,
'has-only-one-job': hasOnlyOneJob(stage),
'append-right-46': shouldAddRightMargin(index),
}" }"
:title="capitalizeStageName(stage.name)" class="stage-column-list align-top"
:groups="stage.groups" >
:stage-connector-class="stageConnectorClass(index, stage)" <stage-column-component
:is-first-column="isFirstColumn(index)" v-for="(stage, index) in graph"
:has-triggered-by="hasTriggeredBy" :key="stage.name"
:action="stage.status.action" :class="{
@refreshPipelineGraph="refreshPipelineGraph" 'has-upstream prepend-left-64': index === 0 && hasTriggeredBy,
/> 'has-downstream': index === graph.length - 1 && hasTriggered,
</ul> 'has-only-one-job': hasOnlyOneJob(stage),
'append-right-46': shouldAddRightMargin(index),
}"
:title="capitalizeStageName(stage.name)"
:groups="stage.groups"
:stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy"
:action="stage.status.action"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
<linked-pipelines-column <linked-pipelines-column
v-if="hasTriggered" v-if="hasTriggered"
:linked-pipelines="triggeredPipelines" :linked-pipelines="triggeredPipelines"
:column-title="__('Downstream')" :column-title="__('Downstream')"
graph-position="right" graph-position="right"
@linkedPipelineClick="handleClickedDownstream" @linkedPipelineClick="handleClickedDownstream"
/> />
<pipeline-graph <pipeline-graph
v-if="type !== $options.upstream && expandedTriggered" v-if="type !== $options.upstream && expandedTriggered"
type="downstream" type="downstream"
class="d-inline-block" class="d-inline-block"
:class="`js-downstream-pipeline-${expandedTriggered.id}`" :class="`js-downstream-pipeline-${expandedTriggered.id}`"
:is-loading="false" :is-loading="false"
:pipeline="expandedTriggered" :pipeline="expandedTriggered"
:is-linked-pipeline="true" :is-linked-pipeline="true"
:style="{ 'margin-top': marginTop }" :style="{ 'margin-top': marginTop }"
:mediator="mediator" :mediator="mediator"
@onClickTriggered=" @onClickTriggered="
(parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline) (parentPipeline, pipeline) => clickTriggeredPipeline(parentPipeline, pipeline)
" "
@refreshPipelineGraph="requestRefreshPipelineGraph" @refreshPipelineGraph="requestRefreshPipelineGraph"
/> />
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -14,6 +14,12 @@ describe('graph component', () => { ...@@ -14,6 +14,12 @@ describe('graph component', () => {
let component; let component;
beforeEach(() => {
setFixtures(`
<div class="layout-page"></div>
`);
});
afterEach(() => { afterEach(() => {
component.$destroy(); component.$destroy();
}); });
...@@ -130,7 +136,7 @@ describe('graph component', () => { ...@@ -130,7 +136,7 @@ describe('graph component', () => {
}); });
describe('with expanded pipeline', () => { describe('with expanded pipeline', () => {
it('should render expanded pipeline', () => { it('should render expanded pipeline', done => {
// expand the pipeline // expand the pipeline
store.state.pipeline.triggered_by[0].isExpanded = true; store.state.pipeline.triggered_by[0].isExpanded = true;
...@@ -140,7 +146,12 @@ describe('graph component', () => { ...@@ -140,7 +146,12 @@ describe('graph component', () => {
mediator, mediator,
}); });
expect(component.$el.querySelector('.js-upstream-pipeline-12')).not.toBeNull(); Vue.nextTick()
.then(() => {
expect(component.$el.querySelector('.js-upstream-pipeline-12')).not.toBeNull();
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
...@@ -161,7 +172,7 @@ describe('graph component', () => { ...@@ -161,7 +172,7 @@ describe('graph component', () => {
}); });
describe('with expanded pipeline', () => { describe('with expanded pipeline', () => {
it('should render expanded pipeline', () => { it('should render expanded pipeline', done => {
// expand the pipeline // expand the pipeline
store.state.pipeline.triggered[0].isExpanded = true; store.state.pipeline.triggered[0].isExpanded = true;
...@@ -171,7 +182,14 @@ describe('graph component', () => { ...@@ -171,7 +182,14 @@ describe('graph component', () => {
mediator, mediator,
}); });
expect(component.$el.querySelector('.js-downstream-pipeline-34993051')).not.toBeNull(); Vue.nextTick()
.then(() => {
expect(
component.$el.querySelector('.js-downstream-pipeline-34993051'),
).not.toBeNull();
})
.then(done)
.catch(done.fail);
}); });
}); });
}); });
......
...@@ -7,6 +7,12 @@ describe('graph component', () => { ...@@ -7,6 +7,12 @@ describe('graph component', () => {
const GraphComponent = Vue.extend(graphComponent); const GraphComponent = Vue.extend(graphComponent);
let component; let component;
beforeEach(() => {
setFixtures(`
<div class="layout-page"></div>
`);
});
afterEach(() => { afterEach(() => {
component.$destroy(); component.$destroy();
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment