Commit ba67fcca authored by Emily Ring's avatar Emily Ring Committed by Natalia Tepluhina

Added loading display for Terraform lock actions

Show loading message when a user is
attempting to lock/unlock a Terraform state
parent f9b2aa2a
<script>
import { GlAlert, GlBadge, GlIcon, GlLink, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
import {
GlAlert,
GlBadge,
GlIcon,
GlLink,
GlLoadingIcon,
GlSprintf,
GlTable,
GlTooltip,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
......@@ -14,6 +23,7 @@ export default {
GlBadge,
GlIcon,
GlLink,
GlLoadingIcon,
GlSprintf,
GlTable,
GlTooltip,
......@@ -67,15 +77,20 @@ export default {
jobStatus: s__('Terraform|Job status'),
locked: s__('Terraform|Locked'),
lockedByUser: s__('Terraform|Locked by %{user} %{timeAgo}'),
lockingState: s__('Terraform|Locking state'),
name: s__('Terraform|Name'),
pipeline: s__('Terraform|Pipeline'),
unknownUser: s__('Terraform|Unknown User'),
unlockingState: s__('Terraform|Unlocking state'),
updatedUser: s__('Terraform|%{user} updated %{timeAgo}'),
},
methods: {
createdByUserName(item) {
return item.latestVersion?.createdByUser?.name;
},
loadingLockText(item) {
return item.lockedAt ? this.$options.i18n.unlockingState : this.$options.i18n.lockingState;
},
lockedByUserName(item) {
return item.lockedByUser?.name || this.$options.i18n.unknownUser;
},
......@@ -119,7 +134,18 @@ export default {
{{ item.name }}
</p>
<div v-if="item.lockedAt" :id="`terraformLockedBadgeContainer${item.name}`" class="gl-mx-2">
<div v-if="item.loadingLock" class="gl-mx-3">
<p class="gl-display-flex gl-justify-content-start gl-align-items-baseline gl-m-0">
<gl-loading-icon class="gl-pr-1" />
{{ loadingLockText(item) }}
</p>
</div>
<div
v-else-if="item.lockedAt"
:id="`terraformLockedBadgeContainer${item.name}`"
class="gl-mx-3"
>
<gl-badge :id="`terraformLockedBadge${item.name}`">
<gl-icon name="lock" />
{{ $options.i18n.locked }}
......
......@@ -64,6 +64,9 @@ export default {
disableModalSubmit() {
return this.removeConfirmText !== this.state.name;
},
loading() {
return this.state.loadingLock || this.state.loadingRemove;
},
primaryModalProps() {
return {
text: this.$options.i18n.modalRemove,
......@@ -77,9 +80,23 @@ export default {
this.removeConfirmText = '';
},
lock() {
this.updateStateCache({
_showDetails: false,
errorMessages: [],
loadingLock: true,
loadingRemove: false,
});
this.stateActionMutation(lockState);
},
unlock() {
this.updateStateCache({
_showDetails: false,
errorMessages: [],
loadingLock: true,
loadingRemove: false,
});
this.stateActionMutation(unlockState);
},
updateStateCache(newData) {
......@@ -96,18 +113,20 @@ export default {
remove() {
if (!this.disableModalSubmit) {
this.hideModal();
this.updateStateCache({
_showDetails: false,
errorMessages: [],
loadingLock: false,
loadingRemove: true,
});
this.stateActionMutation(removeState);
}
},
stateActionMutation(mutation) {
let errorMessages = [];
this.updateStateCache({
_showDetails: false,
errorMessages,
loadingActions: true,
});
this.$apollo
.mutate({
mutation,
......@@ -132,7 +151,8 @@ export default {
this.updateStateCache({
_showDetails: Boolean(errorMessages.length),
errorMessages,
loadingActions: false,
loadingLock: false,
loadingRemove: false,
});
});
},
......@@ -146,7 +166,7 @@ export default {
icon="ellipsis_v"
right
:data-testid="`terraform-state-actions-${state.name}`"
:disabled="state.loadingActions"
:disabled="loading"
toggle-class="gl-px-3! gl-shadow-none!"
>
<template #button-content>
......
......@@ -4,7 +4,8 @@
fragment State on TerraformState {
_showDetails @client
errorMessages @client
loadingActions @client
loadingLock @client
loadingRemove @client
id
name
......
......@@ -9,8 +9,11 @@ export default {
errorMessages: (state) => {
return state.errorMessages || [];
},
loadingActions: (state) => {
return state.loadingActions || false;
loadingLock: (state) => {
return state.loadingLock || false;
},
loadingRemove: (state) => {
return state.loadingRemove || false;
},
},
Mutation: {
......@@ -32,7 +35,8 @@ export default {
// eslint-disable-next-line no-underscore-dangle
_showDetails: terraformState._showDetails,
errorMessages: terraformState.errorMessages,
loadingActions: terraformState.loadingActions,
loadingLock: terraformState.loadingLock,
loadingRemove: terraformState.loadingRemove,
},
});
}
......
---
title: Add loading display to Terraform lock action
merge_request: 53475
author:
type: changed
......@@ -28582,6 +28582,9 @@ msgstr ""
msgid "Terraform|Locked by %{user} %{timeAgo}"
msgstr ""
msgid "Terraform|Locking state"
msgstr ""
msgid "Terraform|Name"
msgstr ""
......@@ -28615,6 +28618,9 @@ msgstr ""
msgid "Terraform|Unlock"
msgstr ""
msgid "Terraform|Unlocking state"
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 ""
......
......@@ -89,17 +89,34 @@ describe('StatesTableActions', () => {
});
describe('when the state is loading', () => {
beforeEach(() => {
return createComponent({
state: {
...defaultProps.state,
loadingActions: true,
},
describe('when lock/unlock is processing', () => {
beforeEach(() => {
return createComponent({
state: {
...defaultProps.state,
loadingLock: true,
},
});
});
it('disables the actions dropdown', () => {
expect(findActionsDropdown().props('disabled')).toBe(true);
});
});
it('disables the actions dropdown', () => {
expect(findActionsDropdown().props('disabled')).toBe(true);
describe('when remove is processing', () => {
beforeEach(() => {
return createComponent({
state: {
...defaultProps.state,
loadingRemove: true,
},
});
});
it('disables the actions dropdown', () => {
expect(findActionsDropdown().props('disabled')).toBe(true);
});
});
});
......@@ -188,7 +205,8 @@ describe('StatesTableActions', () => {
...unlockedProps.state,
_showDetails: false,
errorMessages: [],
loadingActions: true,
loadingLock: true,
loadingRemove: false,
},
},
// Apollo fields
......@@ -205,7 +223,8 @@ describe('StatesTableActions', () => {
...unlockedProps.state,
_showDetails: true,
errorMessages: ['There was an error'],
loadingActions: false,
loadingLock: false,
loadingRemove: false,
},
},
// Apollo fields
......
......@@ -14,6 +14,8 @@ describe('StatesTable', () => {
_showDetails: true,
errorMessages: ['State 1 has errored'],
name: 'state-1',
loadingLock: false,
loadingRemove: false,
lockedAt: '2020-10-13T00:00:00Z',
lockedByUser: {
name: 'user-1',
......@@ -25,6 +27,8 @@ describe('StatesTable', () => {
_showDetails: false,
errorMessages: [],
name: 'state-2',
loadingLock: true,
loadingRemove: false,
lockedAt: null,
lockedByUser: null,
updatedAt: '2020-10-10T00:00:00Z',
......@@ -34,6 +38,8 @@ describe('StatesTable', () => {
_showDetails: false,
errorMessages: [],
name: 'state-3',
loadingLock: true,
loadingRemove: false,
lockedAt: '2020-10-10T00:00:00Z',
lockedByUser: {
name: 'user-2',
......@@ -63,6 +69,8 @@ describe('StatesTable', () => {
_showDetails: true,
errorMessages: ['State 4 has errored'],
name: 'state-4',
loadingLock: false,
loadingRemove: false,
lockedAt: '2020-10-10T00:00:00Z',
lockedByUser: null,
updatedAt: '2020-10-10T00:00:00Z',
......@@ -106,8 +114,8 @@ describe('StatesTable', () => {
it.each`
name | toolTipText | locked | lineNumber
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${0}
${'state-2'} | ${null} | ${false} | ${1}
${'state-3'} | ${'Locked by user-2 5 days ago'} | ${true} | ${2}
${'state-2'} | ${'Locking state'} | ${false} | ${1}
${'state-3'} | ${'Unlocking state'} | ${false} | ${2}
${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${3}
`(
'displays the name and locked information "$name" for line "$lineNumber"',
......
......@@ -2,6 +2,7 @@ import { GlAlert, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTab } from '@git
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import EmptyState from '~/terraform/components/empty_state.vue';
import StatesTable from '~/terraform/components/states_table.vue';
import TerraformList from '~/terraform/components/terraform_list.vue';
......@@ -27,17 +28,20 @@ describe('TerraformList', () => {
},
};
// Override @client _showDetails
getStatesQuery.getStates.definitions[1].selectionSet.selections[0].directives = [];
// Override @client errorMessages
getStatesQuery.getStates.definitions[1].selectionSet.selections[1].directives = [];
// Override @client loadingActions
getStatesQuery.getStates.definitions[1].selectionSet.selections[2].directives = [];
const mockResolvers = {
TerraformState: {
_showDetails: jest.fn().mockResolvedValue(false),
errorMessages: jest.fn().mockResolvedValue([]),
loadingLock: jest.fn().mockResolvedValue(false),
loadingRemove: jest.fn().mockResolvedValue(false),
},
Mutation: {
addDataToTerraformState: jest.fn().mockResolvedValue({}),
},
};
const statsQueryResponse = queryResponse || jest.fn().mockResolvedValue(apolloQueryResponse);
const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]]);
const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]], mockResolvers);
wrapper = shallowMount(TerraformList, {
localVue,
......@@ -66,7 +70,8 @@ describe('TerraformList', () => {
id: 'gid://gitlab/Terraform::State/1',
name: 'state-1',
latestVersion: null,
loadingActions: false,
loadingLock: false,
loadingRemove: false,
lockedAt: null,
lockedByUser: null,
updatedAt: null,
......@@ -77,7 +82,8 @@ describe('TerraformList', () => {
id: 'gid://gitlab/Terraform::State/2',
name: 'state-2',
latestVersion: null,
loadingActions: false,
loadingLock: false,
loadingRemove: false,
lockedAt: null,
lockedByUser: null,
updatedAt: null,
......@@ -98,7 +104,7 @@ describe('TerraformList', () => {
},
});
return wrapper.vm.$nextTick();
return waitForPromises();
});
it('displays a states tab and count', () => {
......@@ -126,7 +132,7 @@ describe('TerraformList', () => {
},
});
return wrapper.vm.$nextTick();
return waitForPromises();
});
it('renders the states table without pagination buttons', () => {
......@@ -146,7 +152,7 @@ describe('TerraformList', () => {
},
});
return wrapper.vm.$nextTick();
return waitForPromises();
});
it('displays a states tab with no count', () => {
......@@ -164,7 +170,7 @@ describe('TerraformList', () => {
beforeEach(() => {
createWrapper({ terraformStates: null, queryResponse: jest.fn().mockRejectedValue() });
return wrapper.vm.$nextTick();
return waitForPromises();
});
it('displays an alert message', () => {
......
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