Commit 125f4aaf authored by Peter Hegman's avatar Peter Hegman

Merge branch '342783-when-user-lacks-permission-still-display-job-manual-action' into 'master'

Disable manual job action button for users without correct permissions

See merge request gitlab-org/gitlab!76959
parents 624fbc5c cf74bdab
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { sprintf } from '~/locale'; import { sprintf, __ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils'; import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue'; import ActionComponent from '../jobs_shared/action_component.vue';
...@@ -160,6 +160,21 @@ export default { ...@@ -160,6 +160,21 @@ export default {
hasAction() { hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path; return this.job.status && this.job.status.action && this.job.status.action.path;
}, },
hasUnauthorizedManualAction() {
return (
!this.hasAction &&
this.job.status?.group === 'manual' &&
this.job.status?.label?.includes('(not allowed)')
);
},
unauthorizedManualActionIcon() {
/*
The action object is not available when the user cannot run the action.
So we can show the correct icon, extract the action name from the label instead:
"manual play action (not allowed)" or "manual stop action (not allowed)"
*/
return this.job.status?.label?.split(' ')[1];
},
relatedDownstreamHovered() { relatedDownstreamHovered() {
return this.job.name === this.sourceJobHovered; return this.job.name === this.sourceJobHovered;
}, },
...@@ -198,6 +213,9 @@ export default { ...@@ -198,6 +213,9 @@ export default {
this.$emit('pipelineActionRequestComplete'); this.$emit('pipelineActionRequestComplete');
}, },
}, },
i18n: {
unauthorizedTooltip: __('You are not authorized to run this manual job'),
},
}; };
</script> </script>
<template> <template>
...@@ -245,5 +263,13 @@ export default { ...@@ -245,5 +263,13 @@ export default {
data-qa-selector="action_button" data-qa-selector="action_button"
@pipelineActionRequestComplete="pipelineActionRequestComplete" @pipelineActionRequestComplete="pipelineActionRequestComplete"
/> />
<action-component
v-if="hasUnauthorizedManualAction"
disabled
:tooltip-text="$options.i18n.unauthorizedTooltip"
:action-icon="unauthorizedManualActionIcon"
:link="`unauthorized-${computedJobId}`"
class="gl-mr-1"
/>
</div> </div>
</template> </template>
...@@ -92,14 +92,20 @@ export default { ...@@ -92,14 +92,20 @@ export default {
<template> <template>
<gl-button <gl-button
:id="`js-ci-action-${link}`" :id="`js-ci-action-${link}`"
v-gl-tooltip="{ boundary: 'viewport' }"
:title="tooltipText"
:class="cssClass" :class="cssClass"
:disabled="isDisabled" :disabled="isDisabled"
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
data-testid="ci-action-component"
@click.stop="onClickAction" @click.stop="onClickAction"
>
<div
v-gl-tooltip.viewport
:title="tooltipText"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-h-full"
data-testid="ci-action-icon-tooltip-wrapper"
> >
<gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" /> <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" />
<gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" /> <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
</div>
</gl-button> </gl-button>
</template> </template>
...@@ -120,6 +120,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) { ...@@ -120,6 +120,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
hasDetails hasDetails
detailsPath detailsPath
group group
label
action { action {
__typename __typename
id id
......
...@@ -40918,6 +40918,9 @@ msgstr "" ...@@ -40918,6 +40918,9 @@ msgstr ""
msgid "You are not authorized to perform this action" msgid "You are not authorized to perform this action"
msgstr "" msgstr ""
msgid "You are not authorized to run this manual job"
msgstr ""
msgid "You are not authorized to update this profile" msgid "You are not authorized to update this profile"
msgstr "" msgstr ""
......
...@@ -30,6 +30,7 @@ Array [ ...@@ -30,6 +30,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "7", "id": "7",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -71,6 +72,7 @@ Array [ ...@@ -71,6 +72,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "12", "id": "12",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -112,6 +114,7 @@ Array [ ...@@ -112,6 +114,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "17", "id": "17",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -153,6 +156,7 @@ Array [ ...@@ -153,6 +156,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "22", "id": "22",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -178,6 +182,7 @@ Array [ ...@@ -178,6 +182,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "25", "id": "25",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -203,6 +208,7 @@ Array [ ...@@ -203,6 +208,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "28", "id": "28",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -237,6 +243,7 @@ Array [ ...@@ -237,6 +243,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "60", "id": "60",
"label": null,
"tooltip": null, "tooltip": null,
}, },
}, },
...@@ -295,6 +302,7 @@ Array [ ...@@ -295,6 +302,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "35", "id": "35",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -348,6 +356,7 @@ Array [ ...@@ -348,6 +356,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "43", "id": "43",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -385,6 +394,7 @@ Array [ ...@@ -385,6 +394,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "50", "id": "50",
"label": "passed",
"tooltip": "passed", "tooltip": "passed",
}, },
}, },
...@@ -423,6 +433,7 @@ Array [ ...@@ -423,6 +433,7 @@ Array [
"hasDetails": true, "hasDetails": true,
"icon": "status_success", "icon": "status_success",
"id": "64", "id": "64",
"label": null,
"tooltip": null, "tooltip": null,
}, },
}, },
......
...@@ -10,6 +10,7 @@ describe('pipeline graph action component', () => { ...@@ -10,6 +10,7 @@ describe('pipeline graph action component', () => {
let wrapper; let wrapper;
let mock; let mock;
const findButton = () => wrapper.find(GlButton); const findButton = () => wrapper.find(GlButton);
const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]');
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -31,14 +32,14 @@ describe('pipeline graph action component', () => { ...@@ -31,14 +32,14 @@ describe('pipeline graph action component', () => {
}); });
it('should render the provided title as a bootstrap tooltip', () => { it('should render the provided title as a bootstrap tooltip', () => {
expect(wrapper.attributes('title')).toBe('bar'); expect(findTooltipWrapper().attributes('title')).toBe('bar');
}); });
it('should update bootstrap tooltip when title changes', async () => { it('should update bootstrap tooltip when title changes', async () => {
wrapper.setProps({ tooltipText: 'changed' }); wrapper.setProps({ tooltipText: 'changed' });
await nextTick(); await nextTick();
expect(wrapper.attributes('title')).toBe('changed'); expect(findTooltipWrapper().attributes('title')).toBe('changed');
}); });
it('should render an svg', () => { it('should render an svg', () => {
......
...@@ -7,6 +7,7 @@ describe('pipeline graph job item', () => { ...@@ -7,6 +7,7 @@ describe('pipeline graph job item', () => {
const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]'); const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]'); const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]');
const findActionComponent = () => wrapper.find('[data-testid="ci-action-component"]');
const createWrapper = (propsData) => { const createWrapper = (propsData) => {
wrapper = mount(JobItem, { wrapper = mount(JobItem, {
...@@ -69,6 +70,19 @@ describe('pipeline graph job item', () => { ...@@ -69,6 +70,19 @@ describe('pipeline graph job item', () => {
hasDetails: false, hasDetails: false,
}, },
}; };
const mockJobWithUnauthorizedAction = {
id: 4258,
name: 'stop-environment',
status: {
icon: 'status_manual',
label: 'manual stop action (not allowed)',
tooltip: 'manual action',
group: 'manual',
detailsPath: '/root/ci-mock/builds/4258',
hasDetails: true,
action: null,
},
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -116,8 +130,21 @@ describe('pipeline graph job item', () => { ...@@ -116,8 +130,21 @@ describe('pipeline graph job item', () => {
it('it should render the action icon', () => { it('it should render the action icon', () => {
createWrapper({ job: mockJob }); createWrapper({ job: mockJob });
expect(wrapper.find('.ci-action-icon-container').exists()).toBe(true); const actionComponent = findActionComponent();
expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true);
expect(actionComponent.exists()).toBe(true);
expect(actionComponent.props('actionIcon')).toBe('retry');
expect(actionComponent.attributes('disabled')).not.toBe('disabled');
});
it('it should render disabled action icon when user cannot run the action', () => {
createWrapper({ job: mockJobWithUnauthorizedAction });
const actionComponent = findActionComponent();
expect(actionComponent.exists()).toBe(true);
expect(actionComponent.props('actionIcon')).toBe('stop');
expect(actionComponent.attributes('disabled')).toBe('disabled');
}); });
}); });
......
...@@ -57,6 +57,7 @@ export const mockPipelineResponse = { ...@@ -57,6 +57,7 @@ export const mockPipelineResponse = {
id: '7', id: '7',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1482', detailsPath: '/root/abcd-dag/-/jobs/1482',
group: 'success', group: 'success',
...@@ -106,6 +107,7 @@ export const mockPipelineResponse = { ...@@ -106,6 +107,7 @@ export const mockPipelineResponse = {
id: '12', id: '12',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1515', detailsPath: '/root/abcd-dag/-/jobs/1515',
group: 'success', group: 'success',
...@@ -155,6 +157,7 @@ export const mockPipelineResponse = { ...@@ -155,6 +157,7 @@ export const mockPipelineResponse = {
id: '17', id: '17',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1484', detailsPath: '/root/abcd-dag/-/jobs/1484',
group: 'success', group: 'success',
...@@ -204,6 +207,7 @@ export const mockPipelineResponse = { ...@@ -204,6 +207,7 @@ export const mockPipelineResponse = {
id: '22', id: '22',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1485', detailsPath: '/root/abcd-dag/-/jobs/1485',
group: 'success', group: 'success',
...@@ -235,6 +239,7 @@ export const mockPipelineResponse = { ...@@ -235,6 +239,7 @@ export const mockPipelineResponse = {
id: '25', id: '25',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1486', detailsPath: '/root/abcd-dag/-/jobs/1486',
group: 'success', group: 'success',
...@@ -266,6 +271,7 @@ export const mockPipelineResponse = { ...@@ -266,6 +271,7 @@ export const mockPipelineResponse = {
id: '28', id: '28',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1487', detailsPath: '/root/abcd-dag/-/jobs/1487',
group: 'success', group: 'success',
...@@ -330,6 +336,7 @@ export const mockPipelineResponse = { ...@@ -330,6 +336,7 @@ export const mockPipelineResponse = {
id: '35', id: '35',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1514', detailsPath: '/root/abcd-dag/-/jobs/1514',
group: 'success', group: 'success',
...@@ -413,6 +420,7 @@ export const mockPipelineResponse = { ...@@ -413,6 +420,7 @@ export const mockPipelineResponse = {
id: '43', id: '43',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1489', detailsPath: '/root/abcd-dag/-/jobs/1489',
group: 'success', group: 'success',
...@@ -498,6 +506,7 @@ export const mockPipelineResponse = { ...@@ -498,6 +506,7 @@ export const mockPipelineResponse = {
id: '50', id: '50',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1490', detailsPath: '/root/abcd-dag/-/jobs/1490',
group: 'success', group: 'success',
...@@ -601,6 +610,7 @@ export const mockPipelineResponse = { ...@@ -601,6 +610,7 @@ export const mockPipelineResponse = {
id: '60', id: '60',
icon: 'status_success', icon: 'status_success',
tooltip: null, tooltip: null,
label: null,
hasDetails: true, hasDetails: true,
detailsPath: '/root/kinder-pipe/-/pipelines/154', detailsPath: '/root/kinder-pipe/-/pipelines/154',
group: 'success', group: 'success',
...@@ -643,6 +653,7 @@ export const mockPipelineResponse = { ...@@ -643,6 +653,7 @@ export const mockPipelineResponse = {
id: '64', id: '64',
icon: 'status_success', icon: 'status_success',
tooltip: null, tooltip: null,
label: null,
hasDetails: true, hasDetails: true,
detailsPath: '/root/abcd-dag/-/pipelines/153', detailsPath: '/root/abcd-dag/-/pipelines/153',
group: 'success', group: 'success',
...@@ -850,6 +861,7 @@ export const wrappedPipelineReturn = { ...@@ -850,6 +861,7 @@ export const wrappedPipelineReturn = {
id: '84', id: '84',
icon: 'status_success', icon: 'status_success',
tooltip: 'passed', tooltip: 'passed',
label: 'passed',
hasDetails: true, hasDetails: true,
detailsPath: '/root/elemenohpee/-/jobs/1662', detailsPath: '/root/elemenohpee/-/jobs/1662',
group: 'success', group: 'success',
......
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