Commit 9c2f94ce authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Phil Hughes

Port of Stop redirecting the page in graph main actions

parent 4614ee14
<script> <script>
import tooltip from '../../../vue_shared/directives/tooltip'; import $ from 'jquery';
import icon from '../../../vue_shared/components/icon.vue'; import tooltip from '../../../vue_shared/directives/tooltip';
import { dasherize } from '../../../lib/utils/text_utility'; import Icon from '../../../vue_shared/components/icon.vue';
/** import { dasherize } from '../../../lib/utils/text_utility';
* Renders either a cancel, retry or play icon pointing to the given path. import eventHub from '../../event_hub';
* TODO: Remove UJS from here and use an async request instead. /**
*/ * Renders either a cancel, retry or play icon pointing to the given path.
export default { */
components: { export default {
icon, components: {
}, Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
tooltipText: { tooltipText: {
type: String, type: String,
required: true, required: true,
}, },
link: { link: {
type: String, type: String,
required: true, required: true,
}, },
actionMethod: { actionIcon: {
type: String, type: String,
required: true, required: true,
}, },
actionIcon: { buttonDisabled: {
type: String, type: String,
required: true, required: false,
}, default: null,
},
},
computed: {
cssClass() {
const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`;
},
isDisabled() {
return this.buttonDisabled === this.link;
}, },
},
computed: { methods: {
cssClass() { onClickAction() {
const actionIconDash = dasherize(this.actionIcon); $(this.$el).tooltip('hide');
return `${actionIconDash} js-icon-${actionIconDash}`; eventHub.$emit('graphAction', this.link);
},
}, },
}; },
};
</script> </script>
<template> <template>
<a <button
type="button"
@click="onClickAction"
v-tooltip v-tooltip
:data-method="actionMethod"
:title="tooltipText" :title="tooltipText"
:href="link" class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper"
class="ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body" data-container="body"
:disabled="isDisabled"
> >
<icon :name="actionIcon" /> <icon :name="actionIcon" />
</a> </button>
</template> </template>
<script> <script>
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import stageColumnComponent from './stage_column_component.vue'; import StageColumnComponent from './stage_column_component.vue';
import LinkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/first
import linkedPipelinesColumn from 'ee/pipelines/components/graph/linked_pipelines_column.vue'; // eslint-disable-line import/first export default {
components: {
export default { LinkedPipelinesColumn,
components: { StageColumnComponent,
linkedPipelinesColumn, LoadingIcon,
stageColumnComponent, },
loadingIcon, props: {
isLoading: {
type: Boolean,
required: true,
},
pipeline: {
type: Object,
required: true,
}, },
props: { actionDisabled: {
isLoading: { type: String,
type: Boolean, required: false,
required: true, default: null,
},
pipeline: {
type: Object,
required: true,
},
}, },
},
computed: { computed: {
graph() { graph() {
return this.pipeline.details && this.pipeline.details.stages; return this.pipeline.details && this.pipeline.details.stages;
}, },
triggered() { triggered() {
return this.pipeline.triggered || []; return this.pipeline.triggered || [];
}, },
triggeredBy() { triggeredBy() {
const response = this.pipeline.triggered_by; const response = this.pipeline.triggered_by;
return response ? [response] : []; return response ? [response] : [];
},
hasTriggered() {
return !!this.triggered.length;
},
hasTriggeredBy() {
return !!this.triggeredBy.length;
},
}, },
hasTriggered() {
return !!this.triggered.length;
},
hasTriggeredBy() {
return !!this.triggeredBy.length;
},
},
methods: { methods: {
capitalizeStageName(name) { capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1); return name.charAt(0).toUpperCase() + name.slice(1);
}, },
isFirstColumn(index) { isFirstColumn(index) {
return index === 0; return index === 0;
}, },
stageConnectorClass(index, stage) { stageConnectorClass(index, stage) {
let className; let className;
// If it's the first stage column and only has one job // If it's the first stage column and only has one job
if (index === 0 && stage.groups.length === 1) { if (index === 0 && stage.groups.length === 1) {
className = 'no-margin'; className = 'no-margin';
} else if (index > 0) { } else if (index > 0) {
// If it is not the first column // If it is not the first column
className = 'left-margin'; className = 'left-margin';
} }
return className; return className;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="build-content middle-block js-pipeline-graph"> <div class="build-content middle-block js-pipeline-graph">
...@@ -101,6 +105,7 @@ ...@@ -101,6 +105,7 @@
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled"
:has-triggered-by="hasTriggeredBy" :has-triggered-by="hasTriggeredBy"
/> />
</ul> </ul>
......
<script> <script>
import actionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue'; import DropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
* Renders the badge for the pipeline graph and the job's dropdown. * Renders the badge for the pipeline graph and the job's dropdown.
* *
* The following object should be provided as `job`: * The following object should be provided as `job`:
* *
* { * {
* "id": 4256, * "id": 4256,
* "name": "test", * "name": "test",
* "status": { * "status": {
* "icon": "icon_status_success", * "icon": "icon_status_success",
* "text": "passed", * "text": "passed",
* "label": "passed", * "label": "passed",
* "group": "success", * "group": "success",
* "tooltip": "passed", * "tooltip": "passed",
* "details_path": "/root/ci-mock/builds/4256", * "details_path": "/root/ci-mock/builds/4256",
* "action": { * "action": {
* "icon": "retry", * "icon": "retry",
* "title": "Retry", * "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry", * "path": "/root/ci-mock/builds/4256/retry",
* "method": "post" * "method": "post"
* } * }
* } * }
* } * }
*/ */
export default { export default {
components: { components: {
actionComponent, ActionComponent,
dropdownActionComponent, DropdownActionComponent,
jobNameComponent, JobNameComponent,
},
directives: {
tooltip,
},
props: {
job: {
type: Object,
required: true,
}, },
directives: { cssClassJobName: {
tooltip, type: String,
required: false,
default: '',
}, },
props: {
job: { isDropdown: {
type: Object, type: Boolean,
required: true, required: false,
}, default: false,
},
cssClassJobName: {
type: String, actionDisabled: {
required: false, type: String,
default: '', required: false,
}, default: null,
},
isDropdown: { },
type: Boolean,
required: false, computed: {
default: false, status() {
}, return this.job && this.job.status ? this.job.status : {};
},
tooltipText() {
const textBuilder = [];
if (this.job.name) {
textBuilder.push(this.job.name);
}
if (this.job.name && this.status.tooltip) {
textBuilder.push('-');
}
if (this.status.tooltip) {
textBuilder.push(`${this.job.status.tooltip}`);
}
return textBuilder.join(' ');
}, },
computed: { /**
status() { * Verifies if the provided job has an action path
return this.job && this.job.status ? this.job.status : {}; *
}, * @return {Boolean}
*/
tooltipText() { hasAction() {
const textBuilder = []; return this.job.status && this.job.status.action && this.job.status.action.path;
if (this.job.name) {
textBuilder.push(this.job.name);
}
if (this.job.name && this.status.tooltip) {
textBuilder.push('-');
}
if (this.status.tooltip) {
textBuilder.push(`${this.job.status.tooltip}`);
}
return textBuilder.join(' ');
},
/**
* Verifies if the provided job has an action path
*
* @return {Boolean}
*/
hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
}, },
}; },
};
</script> </script>
<template> <template>
<div class="ci-job-component"> <div class="ci-job-component">
...@@ -132,7 +138,7 @@ ...@@ -132,7 +138,7 @@
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :button-disabled="actionDisabled"
/> />
<dropdown-action-component <dropdown-action-component
......
<script> <script>
import jobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import dropdownJobComponent from './dropdown_job_component.vue'; import DropdownJobComponent from './dropdown_job_component.vue';
export default { export default {
components: { components: {
jobComponent, JobComponent,
dropdownJobComponent, DropdownJobComponent,
},
props: {
title: {
type: String,
required: true,
}, },
props: {
title: {
type: String,
required: true,
},
jobs: { jobs: {
type: Array, type: Array,
required: true, required: true,
}, },
isFirstColumn: {
type: Boolean,
required: false,
default: false,
},
stageConnectorClass: {
type: String,
required: false,
default: '',
},
isFirstColumn: { actionDisabled: {
type: Boolean, type: String,
required: false, required: false,
default: false, default: null,
}, },
hasTriggeredBy: {
type: Boolean,
required: true,
},
},
stageConnectorClass: { methods: {
type: String, firstJob(list) {
required: false, return list[0];
default: '',
},
hasTriggeredBy: {
type: Boolean,
required: true,
},
}, },
methods: { jobId(job) {
firstJob(list) { return `ci-badge-${job.name}`;
return list[0]; },
},
jobId(job) { buildConnnectorClass(index) {
return `ci-badge-${job.name}`; return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
},
}, },
}; },
};
</script> </script>
<template> <template>
<li <li
...@@ -71,6 +81,7 @@ ...@@ -71,6 +81,7 @@
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
:action-disabled="actionDisabled"
/> />
<dropdown-job-component <dropdown-job-component
......
...@@ -29,13 +29,36 @@ export default () => { ...@@ -29,13 +29,36 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
actionDisabled: null,
}; };
}, },
created() {
eventHub.$on('graphAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('graphAction', this.postAction);
},
methods: {
postAction(action) {
this.actionDisabled = action;
this.mediator.service.postAction(action)
.then(() => {
this.mediator.refreshPipeline();
this.actionDisabled = null;
})
.catch(() => {
this.actionDisabled = null;
Flash(__('An error occurred while making the request.'));
});
},
},
render(createElement) { render(createElement) {
return createElement('pipeline-graph', { return createElement('pipeline-graph', {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled,
}, },
}); });
}, },
......
...@@ -52,8 +52,11 @@ export default class pipelinesMediator { ...@@ -52,8 +52,11 @@ export default class pipelinesMediator {
} }
refreshPipeline() { refreshPipeline() {
this.service.getPipeline() this.poll.stop();
return this.service.getPipeline()
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .catch(() => this.errorCallback())
.finally(() => this.poll.restart());
} }
} }
...@@ -498,41 +498,44 @@ ...@@ -498,41 +498,44 @@
@extend .build-content:hover; @extend .build-content:hover;
} }
// Action Icons in big pipeline-graph nodes .ci-action-icon-container {
.ci-action-icon-container.ci-action-icon-wrapper {
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 5px; top: 5px;
height: 30px;
width: 30px;
background: $white-light;
border: 1px solid $border-color;
border-radius: 100%;
display: block;
&:hover { // Action Icons in big pipeline-graph nodes
background-color: $stage-hover-bg; &.ci-action-icon-wrapper {
border: 1px solid $dropdown-toggle-active-border-color; height: 30px;
width: 30px;
background: $white-light;
border: 1px solid $border-color;
border-radius: 100%;
display: block;
svg { &:hover {
fill: $gl-text-color; background-color: $stage-hover-bg;
} border: 1px solid $dropdown-toggle-active-border-color;
}
svg { svg {
fill: $gl-text-color-secondary; fill: $gl-text-color;
position: relative; }
left: 5px; }
top: 2px;
width: 18px;
height: 18px;
}
&.play {
svg { svg {
width: #{$ci-action-icon-size - 8}; fill: $gl-text-color-secondary;
height: #{$ci-action-icon-size - 8}; position: relative;
left: 8px; left: 5px;
top: 2px;
width: 18px;
height: 18px;
}
&.play {
svg {
width: #{$ci-action-icon-size - 8};
height: #{$ci-action-icon-size - 8};
left: 8px;
}
} }
} }
} }
......
---
title: Stop redirecting the page in pipeline main actions
merge_request:
author:
type: fixed
import Vue from 'vue'; import Vue from 'vue';
import actionComponent from '~/pipelines/components/graph/action_component.vue'; import actionComponent from '~/pipelines/components/graph/action_component.vue';
import eventHub from '~/pipelines/event_hub';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => { describe('pipeline graph action component', () => {
let component; let component;
beforeEach((done) => { beforeEach((done) => {
const ActionComponent = Vue.extend(actionComponent); const ActionComponent = Vue.extend(actionComponent);
component = new ActionComponent({ component = mountComponent(ActionComponent, {
propsData: { tooltipText: 'bar',
tooltipText: 'bar', link: 'foo',
link: 'foo', actionIcon: 'cancel',
actionMethod: 'post', });
actionIcon: 'cancel',
},
}).$mount();
Vue.nextTick(done); Vue.nextTick(done);
}); });
it('should render a link', () => { afterEach(() => {
expect(component.$el.getAttribute('href')).toEqual('foo'); component.$destroy();
});
it('should emit an event with the provided link', () => {
eventHub.$on('graphAction', (link) => {
expect(link).toEqual('foo');
});
}); });
it('should render the provided title as a bootstrap tooltip', () => { it('should render the provided title as a bootstrap tooltip', () => {
......
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