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 @@
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
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 { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
......@@ -160,6 +160,21 @@ export default {
hasAction() {
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() {
return this.job.name === this.sourceJobHovered;
},
......@@ -198,6 +213,9 @@ export default {
this.$emit('pipelineActionRequestComplete');
},
},
i18n: {
unauthorizedTooltip: __('You are not authorized to run this manual job'),
},
};
</script>
<template>
......@@ -245,5 +263,13 @@ export default {
data-qa-selector="action_button"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
<action-component
v-if="hasUnauthorizedManualAction"
disabled
:tooltip-text="$options.i18n.unauthorizedTooltip"
:action-icon="unauthorizedManualActionIcon"
:link="`unauthorized-${computedJobId}`"
class="gl-mr-1"
/>
</div>
</template>
......@@ -92,14 +92,20 @@ export default {
<template>
<gl-button
:id="`js-ci-action-${link}`"
v-gl-tooltip="{ boundary: 'viewport' }"
:title="tooltipText"
:class="cssClass"
: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"
data-testid="ci-action-component"
@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-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
</div>
</gl-button>
</template>
......@@ -120,6 +120,7 @@ query getPipelineDetails($projectPath: ID!, $iid: ID!) {
hasDetails
detailsPath
group
label
action {
__typename
id
......
......@@ -40918,6 +40918,9 @@ msgstr ""
msgid "You are not authorized to perform this action"
msgstr ""
msgid "You are not authorized to run this manual job"
msgstr ""
msgid "You are not authorized to update this profile"
msgstr ""
......
......@@ -30,6 +30,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "7",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -71,6 +72,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "12",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -112,6 +114,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "17",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -153,6 +156,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "22",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -178,6 +182,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "25",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -203,6 +208,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "28",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -237,6 +243,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "60",
"label": null,
"tooltip": null,
},
},
......@@ -295,6 +302,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "35",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -348,6 +356,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "43",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -385,6 +394,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "50",
"label": "passed",
"tooltip": "passed",
},
},
......@@ -423,6 +433,7 @@ Array [
"hasDetails": true,
"icon": "status_success",
"id": "64",
"label": null,
"tooltip": null,
},
},
......
......@@ -10,6 +10,7 @@ describe('pipeline graph action component', () => {
let wrapper;
let mock;
const findButton = () => wrapper.find(GlButton);
const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]');
beforeEach(() => {
mock = new MockAdapter(axios);
......@@ -31,14 +32,14 @@ describe('pipeline graph action component', () => {
});
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 () => {
wrapper.setProps({ tooltipText: 'changed' });
await nextTick();
expect(wrapper.attributes('title')).toBe('changed');
expect(findTooltipWrapper().attributes('title')).toBe('changed');
});
it('should render an svg', () => {
......
......@@ -7,6 +7,7 @@ describe('pipeline graph job item', () => {
const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
const findJobWithLink = () => wrapper.find('[data-testid="job-with-link"]');
const findActionComponent = () => wrapper.find('[data-testid="ci-action-component"]');
const createWrapper = (propsData) => {
wrapper = mount(JobItem, {
......@@ -69,6 +70,19 @@ describe('pipeline graph job item', () => {
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(() => {
wrapper.destroy();
......@@ -116,8 +130,21 @@ describe('pipeline graph job item', () => {
it('it should render the action icon', () => {
createWrapper({ job: mockJob });
expect(wrapper.find('.ci-action-icon-container').exists()).toBe(true);
expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true);
const actionComponent = findActionComponent();
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 = {
id: '7',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1482',
group: 'success',
......@@ -106,6 +107,7 @@ export const mockPipelineResponse = {
id: '12',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1515',
group: 'success',
......@@ -155,6 +157,7 @@ export const mockPipelineResponse = {
id: '17',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1484',
group: 'success',
......@@ -204,6 +207,7 @@ export const mockPipelineResponse = {
id: '22',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1485',
group: 'success',
......@@ -235,6 +239,7 @@ export const mockPipelineResponse = {
id: '25',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1486',
group: 'success',
......@@ -266,6 +271,7 @@ export const mockPipelineResponse = {
id: '28',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1487',
group: 'success',
......@@ -330,6 +336,7 @@ export const mockPipelineResponse = {
id: '35',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1514',
group: 'success',
......@@ -413,6 +420,7 @@ export const mockPipelineResponse = {
id: '43',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1489',
group: 'success',
......@@ -498,6 +506,7 @@ export const mockPipelineResponse = {
id: '50',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/abcd-dag/-/jobs/1490',
group: 'success',
......@@ -601,6 +610,7 @@ export const mockPipelineResponse = {
id: '60',
icon: 'status_success',
tooltip: null,
label: null,
hasDetails: true,
detailsPath: '/root/kinder-pipe/-/pipelines/154',
group: 'success',
......@@ -643,6 +653,7 @@ export const mockPipelineResponse = {
id: '64',
icon: 'status_success',
tooltip: null,
label: null,
hasDetails: true,
detailsPath: '/root/abcd-dag/-/pipelines/153',
group: 'success',
......@@ -850,6 +861,7 @@ export const wrappedPipelineReturn = {
id: '84',
icon: 'status_success',
tooltip: 'passed',
label: 'passed',
hasDetails: true,
detailsPath: '/root/elemenohpee/-/jobs/1662',
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