Commit 59175ca5 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '273592-terraform-delete' into 'master'

Add delete action to terraform list vue

See merge request gitlab-org/gitlab!48485
parents da43ad14 03c0a6cd
<script>
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlFormInput,
GlIcon,
GlModal,
GlSprintf,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
export default {
components: {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlFormGroup,
GlFormInput,
GlIcon,
GlModal,
GlSprintf,
},
props: {
state: {
......@@ -19,20 +34,59 @@ export default {
data() {
return {
loading: false,
showRemoveModal: false,
removeConfirmText: '',
};
},
i18n: {
downloadJSON: s__('Terraform|Download JSON'),
lock: s__('Terraform|Lock'),
modalBody: s__(
'Terraform|You are about to remove the State file %{name}. This will permanently delete all the State versions and history. The infrastructure provisioned previously will remain intact, only the state file with all its versions are to be removed. This action is non-revertible.',
),
modalCancel: s__('Terraform|Cancel'),
modalHeader: s__('Terraform|Are you sure you want to remove the Terraform State %{name}?'),
modalInputLabel: s__(
'Terraform|To remove the State file and its versions, type %{name} to confirm:',
),
modalRemove: s__('Terraform|Remove'),
remove: s__('Terraform|Remove state file and versions'),
unlock: s__('Terraform|Unlock'),
},
computed: {
cancelModalProps() {
return {
text: this.$options.i18n.modalCancel,
attributes: [],
};
},
disableModalSubmit() {
return this.removeConfirmText !== this.state.name;
},
primaryModalProps() {
return {
text: this.$options.i18n.modalRemove,
attributes: [{ disabled: this.disableModalSubmit }, { variant: 'danger' }],
};
},
},
methods: {
hideModal() {
this.showRemoveModal = false;
this.removeConfirmText = '';
},
lock() {
this.stateMutation(lockState);
},
unlock() {
this.stateMutation(unlockState);
},
remove() {
if (!this.disableModalSubmit) {
this.hideModal();
this.stateMutation(removeState);
}
},
stateMutation(mutation) {
this.loading = true;
this.$apollo
......@@ -83,6 +137,56 @@ export default {
<gl-dropdown-item v-else data-testid="terraform-state-lock" @click="lock">
{{ $options.i18n.lock }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-dropdown-item data-testid="terraform-state-remove" @click="showRemoveModal = true">
{{ $options.i18n.remove }}
</gl-dropdown-item>
</gl-dropdown>
<gl-modal
:modal-id="`terraform-state-actions-remove-modal-${state.name}`"
:visible="showRemoveModal"
:action-primary="primaryModalProps"
:action-cancel="cancelModalProps"
@ok="remove"
@cancel="hideModal"
@close="hideModal"
@hide="hideModal"
>
<template #modal-title>
<gl-sprintf :message="$options.i18n.modalHeader">
<template #name>
<span>{{ state.name }}</span>
</template>
</gl-sprintf>
</template>
<p>
<gl-sprintf :message="$options.i18n.modalBody">
<template #name>
<span>{{ state.name }}</span>
</template>
</gl-sprintf>
</p>
<gl-form-group>
<template #label>
<gl-sprintf :message="$options.i18n.modalInputLabel">
<template #name>
<code>{{ state.name }}</code>
</template>
</gl-sprintf>
</template>
<gl-form-input
:id="`terraform-state-remove-input-${state.name}`"
ref="input"
v-model="removeConfirmText"
type="text"
@keyup.enter="remove"
/>
</gl-form-group>
</gl-modal>
</div>
</template>
mutation removeState($stateID: TerraformStateID!) {
terraformStateDelete(input: { id: $stateID }) {
errors
}
}
---
title: Add delete button to terraform list vue
merge_request: 48485
author:
type: added
......@@ -27210,6 +27210,12 @@ msgstr ""
msgid "Terraform|An error occurred while loading your Terraform States"
msgstr ""
msgid "Terraform|Are you sure you want to remove the Terraform State %{name}?"
msgstr ""
msgid "Terraform|Cancel"
msgstr ""
msgid "Terraform|Details"
msgstr ""
......@@ -27243,6 +27249,12 @@ msgstr ""
msgid "Terraform|Pipeline"
msgstr ""
msgid "Terraform|Remove"
msgstr ""
msgid "Terraform|Remove state file and versions"
msgstr ""
msgid "Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete"
msgstr ""
......@@ -27255,12 +27267,18 @@ msgstr ""
msgid "Terraform|The Terraform report %{name} was generated in your pipelines."
msgstr ""
msgid "Terraform|To remove the State file and its versions, type %{name} to confirm:"
msgstr ""
msgid "Terraform|Unknown User"
msgstr ""
msgid "Terraform|Unlock"
msgstr ""
msgid "Terraform|You are about to remove the State file %{name}. This will permanently delete all the State versions and history. The infrastructure provisioned previously\twill remain intact, only the state file with all its versions are to be removed. This action is non-revertible."
msgstr ""
msgid "Test"
msgstr ""
......
......@@ -54,6 +54,24 @@ RSpec.describe 'Terraform', :js do
expect(page).to have_content(terraform_state.name)
end
end
context 'when clicking on the delete button' do
let(:additional_state) { create(:terraform_state, project: project) }
it 'removes the state', :aggregate_failures do
visit project_terraform_index_path(project)
expect(page).to have_content(additional_state.name)
find("[data-testid='terraform-state-actions-#{additional_state.name}']").click
find('[data-testid="terraform-state-remove"]').click
fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name
click_button 'Remove'
expect(page).not_to have_content(additional_state.name)
expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound
end
end
end
end
......
import { GlDropdown } from '@gitlab/ui';
import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import createMockApollo from 'jest/helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
import StateActions from '~/terraform/components/states_table_actions.vue';
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
import unlockStateMutation from '~/terraform/graphql/mutations/unlock_state.mutation.graphql';
const localVue = createLocalVue();
......@@ -11,6 +12,7 @@ localVue.use(VueApollo);
describe('StatesTableActions', () => {
let lockResponse;
let removeResponse;
let unlockResponse;
let wrapper;
......@@ -26,12 +28,17 @@ describe('StatesTableActions', () => {
const createMockApolloProvider = () => {
lockResponse = jest.fn().mockResolvedValue({ data: { terraformStateLock: { errors: [] } } });
removeResponse = jest
.fn()
.mockResolvedValue({ data: { terraformStateDelete: { errors: [] } } });
unlockResponse = jest
.fn()
.mockResolvedValue({ data: { terraformStateUnlock: { errors: [] } } });
return createMockApollo([
[lockStateMutation, lockResponse],
[removeStateMutation, removeResponse],
[unlockStateMutation, unlockResponse],
]);
};
......@@ -43,7 +50,7 @@ describe('StatesTableActions', () => {
apolloProvider,
localVue,
propsData,
stubs: { GlDropdown },
stubs: { GlDropdown, GlModal, GlSprintf },
});
return wrapper.vm.$nextTick();
......@@ -52,6 +59,8 @@ describe('StatesTableActions', () => {
const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]');
const findRemoveModal = () => wrapper.find(GlModal);
beforeEach(() => {
return createComponent();
......@@ -59,6 +68,7 @@ describe('StatesTableActions', () => {
afterEach(() => {
lockResponse = null;
removeResponse = null;
unlockResponse = null;
wrapper.destroy();
});
......@@ -137,4 +147,43 @@ describe('StatesTableActions', () => {
});
});
});
describe('remove button', () => {
it('displays a remove button', () => {
expect(findRemoveBtn().text()).toBe(StateActions.i18n.remove);
});
describe('when clicking the remove button', () => {
beforeEach(() => {
findRemoveBtn().vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('displays a remove modal', () => {
expect(findRemoveModal().text()).toContain(
`You are about to remove the State file ${defaultProps.state.name}`,
);
});
describe('when submitting the remove modal', () => {
it('does not call the remove mutation when state name is missing', async () => {
findRemoveModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
expect(removeResponse).not.toHaveBeenCalledWith();
});
it('calls the remove mutation when state name is present', async () => {
await wrapper.setData({ removeConfirmText: defaultProps.state.name });
findRemoveModal().vm.$emit('ok');
await wrapper.vm.$nextTick();
expect(removeResponse).toHaveBeenCalledWith({
stateID: defaultProps.state.id,
});
});
});
});
});
});
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