Commit 7962ee89 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch 'afontaine/alert-users-when-deleting-deploy-keys' into 'master'

Migrate Deploy Keys Confirmation Modal to GitLab UI

See merge request gitlab-org/gitlab!59697
parents f3824cea 782e6e37
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import eventHub from '../eventhub';
export default {
components: {
GlLoadingIcon,
GlButton,
},
props: {
deployKey: {
......@@ -15,10 +15,20 @@ export default {
type: String,
required: true,
},
btnCssClass: {
category: {
type: String,
required: false,
default: 'btn-default',
default: 'tertiary',
},
variant: {
type: String,
required: false,
default: 'default',
},
icon: {
type: String,
required: false,
default: '',
},
},
data() {
......@@ -39,13 +49,14 @@ export default {
</script>
<template>
<button
:class="[{ disabled: isLoading }, btnCssClass]"
:disabled="isLoading"
<gl-button
:category="category"
:variant="variant"
:icon="icon"
:loading="isLoading"
class="btn"
@click="doAction"
>
<slot></slot>
<gl-loading-icon v-if="isLoading" :inline="true" />
</button>
</gl-button>
</template>
......@@ -6,10 +6,12 @@ import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
import DeployKeysService from '../service';
import DeployKeysStore from '../store';
import ConfirmModal from './confirm_modal.vue';
import KeysPanel from './keys_panel.vue';
export default {
components: {
ConfirmModal,
KeysPanel,
NavigationTabs,
GlLoadingIcon,
......@@ -30,6 +32,9 @@ export default {
currentTab: 'enabled_keys',
isLoading: false,
store: new DeployKeysStore(),
removeKey: () => {},
cancel: () => {},
confirmModalVisible: false,
};
},
scopes: {
......@@ -61,16 +66,16 @@ export default {
this.service = new DeployKeysService(this.endpoint);
eventHub.$on('enable.key', this.enableKey);
eventHub.$on('remove.key', this.disableKey);
eventHub.$on('disable.key', this.disableKey);
eventHub.$on('remove.key', this.confirmRemoveKey);
eventHub.$on('disable.key', this.confirmRemoveKey);
},
mounted() {
this.fetchKeys();
},
beforeDestroy() {
eventHub.$off('enable.key', this.enableKey);
eventHub.$off('remove.key', this.disableKey);
eventHub.$off('disable.key', this.disableKey);
eventHub.$off('remove.key', this.confirmRemoveKey);
eventHub.$off('disable.key', this.confirmRemoveKey);
},
methods: {
onChangeTab(tab) {
......@@ -97,19 +102,20 @@ export default {
.then(this.fetchKeys)
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
disableKey(deployKey, callback) {
if (
// eslint-disable-next-line no-alert
window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))
) {
confirmRemoveKey(deployKey, callback) {
const hideModal = () => {
this.confirmModalVisible = false;
callback?.();
};
this.removeKey = () => {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
.then(callback)
.then(hideModal)
.catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
} else {
callback();
}
};
this.cancel = hideModal;
this.confirmModalVisible = true;
},
},
};
......@@ -117,6 +123,7 @@ export default {
<template>
<div class="gl-mb-3 deploy-keys">
<confirm-modal :visible="confirmModalVisible" @remove="removeKey" @cancel="cancel" />
<gl-loading-icon
v-if="isLoading && !hasKeys"
:label="s__('DeployKeys|Loading deploy keys')"
......@@ -124,8 +131,12 @@ export default {
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
<div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
<div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
<div class="fade-left">
<gl-icon name="chevron-lg-left" :size="12" />
</div>
<div class="fade-right">
<gl-icon name="chevron-lg-right" :size="12" />
</div>
<navigation-tabs :tabs="tabs" scope="deployKeys" @onChangeTab="onChangeTab" />
</div>
......
<script>
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlModal,
},
props: {
visible: {
type: Boolean,
required: false,
default: false,
},
},
i18n: {
body: __(
'Are you sure you want to remove this deploy key? If anything is still using this key, it will stop working.',
),
},
modalOptions: {
title: __('Do you want to remove this deploy key?'),
actionPrimary: {
text: __('Remove deploy key'),
attributes: [{ variant: 'danger' }],
},
actionSecondary: {
text: __('Cancel'),
attributes: [{ category: 'tertiary' }],
},
static: true,
modalId: 'confirm-remove-deploy-key',
},
};
</script>
<template>
<gl-modal
v-bind="$options.modalOptions"
:visible="visible"
@primary="$emit('remove')"
@secondary="$emit('cancel')"
@hidden="$emit('cancel')"
>
{{ $options.i18n.body }}
</gl-modal>
</template>
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlIcon, GlLink, GlTooltipDirective, GlButton } from '@gitlab/ui';
import { head, tail } from 'lodash';
import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
......@@ -9,7 +9,9 @@ import actionBtn from './action_btn.vue';
export default {
components: {
actionBtn,
GlButton,
GlIcon,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -123,15 +125,15 @@ export default {
<div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Project usage') }}</div>
<div class="table-mobile-content deploy-project-list">
<template v-if="projects.length > 0">
<a
<gl-link
v-gl-tooltip
:title="projectTooltipTitle(firstProject)"
class="label deploy-project-label"
>
<span> {{ firstProject.project.full_name }} </span>
<gl-icon :name="firstProject.can_push ? 'lock-open' : 'lock'" />
</a>
<a
</gl-link>
<gl-link
v-if="isExpandable"
v-gl-tooltip
:title="restProjectsTooltip"
......@@ -139,8 +141,8 @@ export default {
@click="toggleExpanded"
>
<span>{{ restProjectsLabel }}</span>
</a>
<a
</gl-link>
<gl-link
v-for="deployKeysProject in restProjects"
v-else-if="isExpanded"
:key="deployKeysProject.project.full_path"
......@@ -151,7 +153,7 @@ export default {
>
<span> {{ deployKeysProject.project.full_name }} </span>
<gl-icon :name="deployKeysProject.can_push ? 'lock-open' : 'lock'" />
</a>
</gl-link>
</template>
<span v-else class="text-secondary">{{ __('None') }}</span>
</div>
......@@ -166,41 +168,43 @@ export default {
</div>
<div class="table-section section-15 table-button-footer deploy-key-actions">
<div class="btn-group table-action-buttons">
<action-btn v-if="!isEnabled" :deploy-key="deployKey" type="enable">
<action-btn v-if="!isEnabled" :deploy-key="deployKey" type="enable" category="secondary">
{{ __('Enable') }}
</action-btn>
<a
<gl-button
v-if="deployKey.can_edit"
v-gl-tooltip
:href="editDeployKeyPath"
:title="__('Edit')"
class="btn btn-default text-secondary"
:aria-label="__('Edit')"
data-container="body"
>
<gl-icon name="pencil" />
</a>
icon="pencil"
category="secondary"
/>
<action-btn
v-if="isRemovable"
v-gl-tooltip
:deploy-key="deployKey"
:title="__('Remove')"
btn-css-class="btn-danger"
:aria-label="__('Remove')"
category="primary"
variant="danger"
icon="remove"
type="remove"
data-container="body"
>
<gl-icon name="remove" />
</action-btn>
/>
<action-btn
v-else-if="isEnabled"
v-gl-tooltip
:deploy-key="deployKey"
:title="__('Disable')"
btn-css-class="btn-warning"
:aria-label="__('Disable')"
type="disable"
data-container="body"
>
<gl-icon name="cancel" />
</action-btn>
icon="cancel"
category="primary"
variant="danger"
/>
</div>
</div>
</div>
......
---
title: Use GlModal for Confirmation of Deploy Key Delete
merge_request: 59697
author:
type: changed
......@@ -151,6 +151,24 @@ Adding a public deploy key does not immediately expose any repository to it. Pub
deploy keys enable access from other systems, but access is not given to any project
until a project maintainer chooses to make use of it.
## How to disable deploy keys
[Project maintainers and owners](../../permissions.md#project-members-permissions)
can remove or disable a deploy key for a project repository:
1. Navigate to the project's **Settings > Repository** page.
1. Expand the **Deploy keys** section.
1. Select the **{remove}** or **{cancel}** button.
NOTE:
If anything relies on the removed deploy key, it will stop working once removed.
If the key is **publicly accessible**, it will be removed from the project, but still available under **Publicly accessible deploy keys**.
If the key is **privately accessible** and only in use by this project, it will deleted.
If the key is **privately accessible** and in use by other projects, it will be removed from the project, but still available under **Privately accesible deploy keys**.
## Troubleshooting
### Deploy key cannot push to a protected branch
......
......@@ -96,9 +96,8 @@ RSpec.describe 'Projects > Audit Events', :js do
visit project_deploy_keys_path(project)
accept_confirm do
find('[data-testid="remove-icon"]').click
end
click_button 'Remove'
click_button 'Remove deploy key'
visit project_audit_events_path(project)
......
......@@ -4265,6 +4265,9 @@ msgstr ""
msgid "Are you sure you want to remove the license?"
msgstr ""
msgid "Are you sure you want to remove this deploy key? If anything is still using this key, it will stop working."
msgstr ""
msgid "Are you sure you want to remove this identity?"
msgstr ""
......@@ -10816,9 +10819,6 @@ msgstr ""
msgid "DeployKeys|Read access only"
msgstr ""
msgid "DeployKeys|You are going to remove this deploy key. Are you sure?"
msgstr ""
msgid "DeployTokens|Active Deploy Tokens (%{active_tokens})"
msgstr ""
......@@ -11483,6 +11483,9 @@ msgstr ""
msgid "Do not display offers from third parties within GitLab"
msgstr ""
msgid "Do you want to remove this deploy key?"
msgstr ""
msgid "Dockerfile"
msgstr ""
......@@ -26466,6 +26469,9 @@ msgstr ""
msgid "Remove child epic from an epic"
msgstr ""
msgid "Remove deploy key"
msgstr ""
msgid "Remove description history"
msgstr ""
......
......@@ -22,7 +22,8 @@ RSpec.describe 'Project deploy keys', :js do
page.within(find('.qa-deploy-keys-settings')) do
expect(page).to have_selector('.deploy-key', count: 1)
accept_confirm { find('[data-testid="remove-icon"]').click }
click_button 'Remove'
click_button 'Remove deploy key'
wait_for_requests
......
......@@ -117,7 +117,8 @@ RSpec.describe 'Projects > Settings > Repository settings' do
project.deploy_keys << private_deploy_key
visit project_settings_repository_path(project)
accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('[data-testid="remove-icon"]').click }
click_button 'Remove'
click_button 'Remove deploy key'
expect(page).not_to have_content(private_deploy_key.title)
end
......
import { GlLoadingIcon } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import actionBtn from '~/deploy_keys/components/action_btn.vue';
import eventHub from '~/deploy_keys/eventhub';
......@@ -8,13 +8,16 @@ describe('Deploy keys action btn', () => {
const deployKey = data.enabled_keys[0];
let wrapper;
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
wrapper = shallowMount(actionBtn, {
propsData: {
deployKey,
type: 'enable',
category: 'primary',
variant: 'confirm',
icon: 'edit',
},
slots: {
default: 'Enable',
......@@ -26,10 +29,18 @@ describe('Deploy keys action btn', () => {
expect(wrapper.text()).toBe('Enable');
});
it('passes the button props on', () => {
expect(findButton().props()).toMatchObject({
category: 'primary',
variant: 'confirm',
icon: 'edit',
});
});
it('sends eventHub event with btn type', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
wrapper.trigger('click');
findButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything());
......@@ -37,18 +48,10 @@ describe('Deploy keys action btn', () => {
});
it('shows loading spinner after click', () => {
wrapper.trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(findLoadingIcon().exists()).toBe(true);
});
});
it('disables button after click', () => {
wrapper.trigger('click');
findButton().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.attributes('disabled')).toBe('disabled');
expect(findButton().props('loading')).toBe(true);
});
});
});
......@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import deployKeysApp from '~/deploy_keys/components/app.vue';
import ConfirmModal from '~/deploy_keys/components/confirm_modal.vue';
import eventHub from '~/deploy_keys/eventhub';
import axios from '~/lib/utils/axios_utils';
......@@ -36,6 +37,7 @@ describe('Deploy keys app component', () => {
const findLoadingIcon = () => wrapper.find('.gl-spinner');
const findKeyPanels = () => wrapper.findAll('.deploy-keys .gl-tabs-nav li');
const findModal = () => wrapper.findComponent(ConfirmModal);
it('renders loading icon while waiting for request', () => {
mock.onGet(TEST_ENDPOINT).reply(() => new Promise());
......@@ -94,11 +96,16 @@ describe('Deploy keys app component', () => {
const key = data.public_keys[0];
return mountComponent()
.then(() => {
jest.spyOn(window, 'confirm').mockReturnValue(true);
jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
eventHub.$emit('disable.key', key);
eventHub.$emit('disable.key', key, () => {});
return wrapper.vm.$nextTick();
})
.then(() => {
expect(findModal().props('visible')).toBe(true);
findModal().vm.$emit('remove');
return wrapper.vm.$nextTick();
})
......@@ -112,11 +119,16 @@ describe('Deploy keys app component', () => {
const key = data.public_keys[0];
return mountComponent()
.then(() => {
jest.spyOn(window, 'confirm').mockReturnValue(true);
jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
eventHub.$emit('remove.key', key);
eventHub.$emit('remove.key', key, () => {});
return wrapper.vm.$nextTick();
})
.then(() => {
expect(findModal().props('visible')).toBe(true);
findModal().vm.$emit('remove');
return wrapper.vm.$nextTick();
})
......
import { GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ConfirmModal from '~/deploy_keys/components/confirm_modal.vue';
describe('~/deploy_keys/components/confirm_modal.vue', () => {
let wrapper;
let modal;
beforeEach(() => {
wrapper = mount(ConfirmModal, { propsData: { modalId: 'test', visible: true } });
modal = extendedWrapper(wrapper.findComponent(GlModal));
});
it('emits a remove event if the primary button is clicked', () => {
modal.findByText('Remove deploy key').trigger('click');
expect(wrapper.emitted('remove')).toEqual([[]]);
});
it('emits a cancel event if the secondary button is clicked', () => {
modal.findByText('Cancel').trigger('click');
expect(wrapper.emitted('cancel')).toEqual([[]]);
});
it('displays the warning about removing the deploy key', () => {
expect(modal.text()).toContain('Are you sure you want to remove this deploy key?');
});
});
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