Commit cf74bdab authored by Miranda Fluharty's avatar Miranda Fluharty Committed by Peter Hegman

Show disabled CI action icon when unauthorized

When a user is not authorized to run a manual CI action,
show the action button component in a disabled state
and add a tooltip explaining that the action is not allowed

Changelog: changed
parent 094e8485
......@@ -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"
>
<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
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
......
......@@ -40903,6 +40903,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