Commit 59e06a7b authored by Phil Hughes's avatar Phil Hughes

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

Add lock/unlock buttons to the Terraform State listing

See merge request gitlab-org/gitlab!47842
parents ed373b4d d4074416
......@@ -52,12 +52,18 @@ export default {
return columns;
},
},
i18n: {
locked: s__('Terraform|Locked'),
lockedByUser: s__('Terraform|Locked by %{user} %{timeAgo}'),
unknownUser: s__('Terraform|Unknown User'),
updatedUser: s__('Terraform|%{user} updated %{timeAgo}'),
},
methods: {
createdByUserName(item) {
return item.latestVersion?.createdByUser?.name;
},
lockedByUserName(item) {
return item.lockedByUser?.name || s__('Terraform|Unknown User');
return item.lockedByUser?.name || this.$options.i18n.unknownUser;
},
updatedTime(item) {
return item.latestVersion?.updatedAt || item.updatedAt;
......@@ -74,18 +80,18 @@ export default {
{{ item.name }}
</p>
<div v-if="item.lockedAt" id="terraformLockedBadgeContainer" class="gl-mx-2">
<gl-badge id="terraformLockedBadge">
<div v-if="item.lockedAt" :id="`terraformLockedBadgeContainer${item.name}`" class="gl-mx-2">
<gl-badge :id="`terraformLockedBadge${item.name}`">
<gl-icon name="lock" />
{{ s__('Terraform|Locked') }}
{{ $options.i18n.locked }}
</gl-badge>
<gl-tooltip
container="terraformLockedBadgeContainer"
:container="`terraformLockedBadgeContainer${item.name}`"
:target="`terraformLockedBadge${item.name}`"
placement="right"
target="terraformLockedBadge"
>
<gl-sprintf :message="s__('Terraform|Locked by %{user} %{timeAgo}')">
<gl-sprintf :message="$options.i18n.lockedByUser">
<template #user>
{{ lockedByUserName(item) }}
</template>
......@@ -101,7 +107,7 @@ export default {
<template #cell(updated)="{ item }">
<p class="gl-m-0" data-testid="terraform-states-table-updated">
<gl-sprintf :message="s__('Terraform|%{user} updated %{timeAgo}')">
<gl-sprintf :message="$options.i18n.updatedUser">
<template #user>
<span v-if="item.latestVersion">
{{ createdByUserName(item) }}
......
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlIcon } 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';
export default {
components: {
GlDropdown,
GlDropdownItem,
GlIcon,
},
props: {
state: {
......@@ -13,22 +16,73 @@ export default {
type: Object,
},
},
data() {
return {
loading: false,
};
},
i18n: {
downloadJSON: s__('Terraform|Download JSON'),
lock: s__('Terraform|Lock'),
unlock: s__('Terraform|Unlock'),
},
methods: {
lock() {
this.stateMutation(lockState);
},
unlock() {
this.stateMutation(unlockState);
},
stateMutation(mutation) {
this.loading = true;
this.$apollo
.mutate({
mutation,
variables: {
stateID: this.state.id,
},
refetchQueries: () => ['getStates'],
awaitRefetchQueries: true,
notifyOnNetworkStatusChange: true,
})
.catch(() => {})
.finally(() => {
this.loading = false;
});
},
},
};
</script>
<template>
<div v-if="state.latestVersion">
<gl-dropdown icon="ellipsis_v" right :data-testid="`terraform-state-actions-${state.name}`">
<div>
<gl-dropdown
icon="ellipsis_v"
right
:data-testid="`terraform-state-actions-${state.name}`"
:disabled="loading"
toggle-class="gl-px-3! gl-shadow-none!"
>
<template #button-content>
<gl-icon class="gl-mr-0" name="ellipsis_v" />
</template>
<gl-dropdown-item
v-if="state.latestVersion"
data-testid="terraform-state-download"
:download="`${state.name}.json`"
:href="state.latestVersion.downloadPath"
>
{{ $options.i18n.downloadJSON }}
</gl-dropdown-item>
<gl-dropdown-item v-if="state.lockedAt" data-testid="terraform-state-unlock" @click="unlock">
{{ $options.i18n.unlock }}
</gl-dropdown-item>
<gl-dropdown-item v-else data-testid="terraform-state-lock" @click="lock">
{{ $options.i18n.lock }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
......@@ -15,13 +15,7 @@ export default {
...this.cursor,
};
},
update: data => {
return {
count: data?.project?.terraformStates?.count,
list: data?.project?.terraformStates?.nodes,
pageInfo: data?.project?.terraformStates?.pageInfo,
};
},
update: data => data,
error() {
this.states = null;
},
......@@ -67,35 +61,34 @@ export default {
return this.$apollo.queries.states.loading;
},
pageInfo() {
return this.states?.pageInfo || {};
return this.states?.project?.terraformStates?.pageInfo || {};
},
showPagination() {
return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
},
statesCount() {
return this.states?.count;
return this.states?.project?.terraformStates?.count;
},
statesList() {
return this.states?.list;
return this.states?.project?.terraformStates?.nodes;
},
},
methods: {
updatePagination(item) {
if (item === this.pageInfo.endCursor) {
this.cursor = {
first: MAX_LIST_COUNT,
after: item,
last: null,
before: null,
};
} else {
this.cursor = {
first: null,
after: null,
last: MAX_LIST_COUNT,
before: item,
};
}
nextPage(item) {
this.cursor = {
first: MAX_LIST_COUNT,
after: item,
last: null,
before: null,
};
},
prevPage(item) {
this.cursor = {
first: null,
after: null,
last: MAX_LIST_COUNT,
before: item,
};
},
},
};
......@@ -119,11 +112,7 @@ export default {
<states-table :states="statesList" :terraform-admin="terraformAdmin" />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination
v-bind="pageInfo"
@prev="updatePagination"
@next="updatePagination"
/>
<gl-keyset-pagination v-bind="pageInfo" @prev="prevPage" @next="nextPage" />
</div>
</div>
......
mutation lockState($stateID: TerraformStateID!) {
terraformStateLock(input: { id: $stateID }) {
errors
}
}
mutation unlockState($stateID: TerraformStateID!) {
terraformStateUnlock(input: { id: $stateID }) {
errors
}
}
---
title: Add lock button to the Terraform State list view
merge_request: 47842
author:
type: added
......@@ -27081,6 +27081,9 @@ msgstr ""
msgid "Terraform|Get started with Terraform"
msgstr ""
msgid "Terraform|Lock"
msgstr ""
msgid "Terraform|Locked"
msgstr ""
......@@ -27102,6 +27105,9 @@ msgstr ""
msgid "Terraform|Unknown User"
msgstr ""
msgid "Terraform|Unlock"
msgstr ""
msgid "Test"
msgstr ""
......
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
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 unlockStateMutation from '~/terraform/graphql/mutations/unlock_state.mutation.graphql';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('StatesTableActions', () => {
let lockResponse;
let unlockResponse;
let wrapper;
const defaultProps = {
......@@ -10,11 +19,29 @@ describe('StatesTableActions', () => {
id: 'gid/1',
name: 'state-1',
latestVersion: { downloadPath: '/path' },
lockedAt: '2020-10-13T00:00:00Z',
},
};
const createMockApolloProvider = () => {
lockResponse = jest.fn().mockResolvedValue({ data: { terraformStateLock: { errors: [] } } });
unlockResponse = jest
.fn()
.mockResolvedValue({ data: { terraformStateUnlock: { errors: [] } } });
return createMockApollo([
[lockStateMutation, lockResponse],
[unlockStateMutation, unlockResponse],
]);
};
const createComponent = (propsData = defaultProps) => {
const apolloProvider = createMockApolloProvider();
wrapper = shallowMount(StateActions, {
apolloProvider,
localVue,
propsData,
stubs: { GlDropdown },
});
......@@ -22,37 +49,92 @@ describe('StatesTableActions', () => {
return wrapper.vm.$nextTick();
};
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"]');
beforeEach(() => {
return createComponent();
});
afterEach(() => {
lockResponse = null;
unlockResponse = null;
wrapper.destroy();
});
describe('when state has a latestVersion', () => {
beforeEach(() => {
return createComponent();
describe('download button', () => {
it('displays a download button', () => {
expect(findDownloadBtn().text()).toBe('Download JSON');
});
it('displays a download button', () => {
const downloadBtn = findDownloadBtn();
describe('when state does not have a latestVersion', () => {
beforeEach(() => {
return createComponent({
state: {
id: 'gid/1',
name: 'state-1',
latestVersion: null,
},
});
});
expect(downloadBtn.text()).toBe('Download JSON');
it('does not display a download button', () => {
expect(findDownloadBtn().exists()).toBe(false);
});
});
});
describe('when state does not have a latestVersion', () => {
beforeEach(() => {
return createComponent({
state: {
id: 'gid/1',
name: 'state-1',
latestVersion: null,
},
describe('unlock button', () => {
it('displays an unlock button', () => {
expect(findUnlockBtn().text()).toBe('Unlock');
expect(findLockBtn().exists()).toBe(false);
});
describe('when clicking the unlock button', () => {
beforeEach(() => {
findUnlockBtn().vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('calls the unlock mutation', () => {
expect(unlockResponse).toHaveBeenCalledWith({
stateID: defaultProps.state.id,
});
});
});
});
describe('lock button', () => {
const unlockedProps = {
state: {
id: 'gid/2',
name: 'state-2',
latestVersion: null,
lockedAt: null,
},
};
beforeEach(() => {
return createComponent(unlockedProps);
});
it('displays a lock button', () => {
expect(findLockBtn().text()).toBe('Lock');
expect(findUnlockBtn().exists()).toBe(false);
});
it('does not display a download button', () => {
expect(findDownloadBtn().exists()).toBe(false);
describe('when clicking the lock button', () => {
beforeEach(() => {
findLockBtn().vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('calls the lock mutation', () => {
expect(lockResponse).toHaveBeenCalledWith({
stateID: unlockedProps.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