Commit 1cace160 authored by Thong Kuah's avatar Thong Kuah

Merge branch '273592-download-action' into 'master'

Add download action to the Terraform state listing

See merge request gitlab-org/gitlab!48837
parents 411b8015 00b5b22f
<script> <script>
import { GlBadge, GlIcon, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui'; import { GlBadge, GlIcon, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import StateActions from './states_table_actions.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
GlSprintf, GlSprintf,
GlTable, GlTable,
GlTooltip, GlTooltip,
StateActions,
TimeAgoTooltip, TimeAgoTooltip,
}, },
mixins: [timeagoMixin], mixins: [timeagoMixin],
...@@ -19,10 +21,15 @@ export default { ...@@ -19,10 +21,15 @@ export default {
required: true, required: true,
type: Array, type: Array,
}, },
terraformAdmin: {
required: false,
type: Boolean,
default: false,
},
}, },
computed: { computed: {
fields() { fields() {
return [ const columns = [
{ {
key: 'name', key: 'name',
thClass: 'gl-display-none', thClass: 'gl-display-none',
...@@ -33,6 +40,16 @@ export default { ...@@ -33,6 +40,16 @@ export default {
tdClass: 'gl-text-right', tdClass: 'gl-text-right',
}, },
]; ];
if (this.terraformAdmin) {
columns.push({
key: 'actions',
thClass: 'gl-display-none',
tdClass: 'gl-w-10',
});
}
return columns;
}, },
}, },
methods: { methods: {
...@@ -97,5 +114,9 @@ export default { ...@@ -97,5 +114,9 @@ export default {
</gl-sprintf> </gl-sprintf>
</p> </p>
</template> </template>
<template v-if="terraformAdmin" #cell(actions)="{ item }">
<state-actions :state="item" />
</template>
</gl-table> </gl-table>
</template> </template>
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
components: {
GlDropdown,
GlDropdownItem,
},
props: {
state: {
required: true,
type: Object,
},
},
i18n: {
downloadJSON: s__('Terraform|Download JSON'),
},
};
</script>
<template>
<div v-if="state.latestVersion">
<gl-dropdown icon="ellipsis_v" right :data-testid="`terraform-state-actions-${state.name}`">
<gl-dropdown-item
data-testid="terraform-state-download"
:download="`${state.name}.json`"
:href="state.latestVersion.downloadPath"
>
{{ $options.i18n.downloadJSON }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
...@@ -46,6 +46,11 @@ export default { ...@@ -46,6 +46,11 @@ export default {
required: true, required: true,
type: String, type: String,
}, },
terraformAdmin: {
required: false,
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
...@@ -111,7 +116,7 @@ export default { ...@@ -111,7 +116,7 @@ export default {
<div v-else-if="statesList"> <div v-else-if="statesList">
<div v-if="statesCount"> <div v-if="statesCount">
<states-table :states="statesList" /> <states-table :states="statesList" :terraform-admin="terraformAdmin" />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5"> <div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination <gl-keyset-pagination
......
#import "~/graphql_shared/fragments/user.fragment.graphql" #import "~/graphql_shared/fragments/user.fragment.graphql"
fragment StateVersion on TerraformStateVersion { fragment StateVersion on TerraformStateVersion {
downloadPath
serial
updatedAt updatedAt
createdByUser { createdByUser {
......
...@@ -24,6 +24,7 @@ export default () => { ...@@ -24,6 +24,7 @@ export default () => {
props: { props: {
emptyStateImage, emptyStateImage,
projectPath, projectPath,
terraformAdmin: el.hasAttribute('data-terraform-admin'),
}, },
}); });
}, },
......
# frozen_string_literal: true # frozen_string_literal: true
module Projects::TerraformHelper module Projects::TerraformHelper
def js_terraform_list_data(project) def js_terraform_list_data(current_user, project)
{ {
empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'), empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'),
project_path: project.full_path project_path: project.full_path,
terraform_admin: current_user&.can?(:admin_terraform_state, project)
} }
end end
end end
- breadcrumb_title _('Terraform') - breadcrumb_title _('Terraform')
- page_title _('Terraform') - page_title _('Terraform')
#js-terraform-list{ data: js_terraform_list_data(@project) } #js-terraform-list{ data: js_terraform_list_data(current_user, @project) }
---
title: Add download action to the Terraform state listing
merge_request: 48837
author:
type: added
...@@ -26946,6 +26946,9 @@ msgstr "" ...@@ -26946,6 +26946,9 @@ msgstr ""
msgid "Terraform|An error occurred while loading your Terraform States" msgid "Terraform|An error occurred while loading your Terraform States"
msgstr "" msgstr ""
msgid "Terraform|Download JSON"
msgstr ""
msgid "Terraform|Find out how to use the %{linkStart}GitLab managed Terraform State%{linkEnd}" msgid "Terraform|Find out how to use the %{linkStart}GitLab managed Terraform State%{linkEnd}"
msgstr "" msgstr ""
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Projects::TerraformController do RSpec.describe Projects::TerraformController do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project, :public) }
describe 'GET index' do describe 'GET index' do
subject { get :index, params: { namespace_id: project.namespace, project_id: project } } subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
...@@ -34,5 +34,15 @@ RSpec.describe Projects::TerraformController do ...@@ -34,5 +34,15 @@ RSpec.describe Projects::TerraformController do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'when no user is present' do
before do
subject
end
it 'shows 404' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
end end
end end
...@@ -4,16 +4,21 @@ require 'spec_helper' ...@@ -4,16 +4,21 @@ require 'spec_helper'
RSpec.describe 'Terraform', :js do RSpec.describe 'Terraform', :js do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:terraform_state) { create(:terraform_state, :locked, :with_version, project: project) }
let(:user) { project.creator } context 'when user is a terraform administrator' do
let(:admin) { project.creator }
before do before do
gitlab_sign_in(user) gitlab_sign_in(admin)
end end
context 'when user does not have any terraform states and visits index page' do context 'when user does not have any terraform states and visits the index page' do
let(:empty_project) { create(:project) }
before do before do
visit project_terraform_index_path(project) empty_project.add_maintainer(admin)
visit project_terraform_index_path(empty_project)
end end
it 'sees an empty state' do it 'sees an empty state' do
...@@ -22,8 +27,6 @@ RSpec.describe 'Terraform', :js do ...@@ -22,8 +27,6 @@ RSpec.describe 'Terraform', :js do
end end
context 'when user has a terraform state' do context 'when user has a terraform state' do
let_it_be(:terraform_state) { create(:terraform_state, :locked, project: project) }
context 'when user visits the index page' do context 'when user visits the index page' do
before do before do
visit project_terraform_index_path(project) visit project_terraform_index_path(project)
...@@ -35,7 +38,14 @@ RSpec.describe 'Terraform', :js do ...@@ -35,7 +38,14 @@ RSpec.describe 'Terraform', :js do
it 'displays a table with terraform states' do it 'displays a table with terraform states' do
expect(page).to have_selector( expect(page).to have_selector(
'[data-testid="terraform-states-table"] tbody tr', '[data-testid="terraform-states-table-name"]',
count: project.terraform_states.size
)
end
it 'displays terraform actions dropdown' do
expect(page).to have_selector(
'[data-testid*="terraform-state-actions"]',
count: project.terraform_states.size count: project.terraform_states.size
) )
end end
...@@ -45,4 +55,26 @@ RSpec.describe 'Terraform', :js do ...@@ -45,4 +55,26 @@ RSpec.describe 'Terraform', :js do
end end
end end
end end
end
context 'when user is a terraform developer' do
let_it_be(:developer) { create(:user) }
before do
project.add_developer(developer)
gitlab_sign_in(developer)
visit project_terraform_index_path(project)
end
context 'when user visits the index page' do
it 'displays a table without an action dropdown', :aggregate_failures do
expect(page).to have_selector(
'[data-testid="terraform-states-table-name"]',
count: project.terraform_states.size
)
expect(page).not_to have_selector('[data-testid*="terraform-state-actions"]')
end
end
end
end end
import { GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StateActions from '~/terraform/components/states_table_actions.vue';
describe('StatesTableActions', () => {
let wrapper;
const defaultProps = {
state: {
id: 'gid/1',
name: 'state-1',
latestVersion: { downloadPath: '/path' },
},
};
const createComponent = (propsData = defaultProps) => {
wrapper = shallowMount(StateActions, {
propsData,
stubs: { GlDropdown },
});
return wrapper.vm.$nextTick();
};
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
afterEach(() => {
wrapper.destroy();
});
describe('when state has a latestVersion', () => {
beforeEach(() => {
return createComponent();
});
it('displays a download button', () => {
const downloadBtn = findDownloadBtn();
expect(downloadBtn.text()).toBe('Download JSON');
});
});
describe('when state does not have a latestVersion', () => {
beforeEach(() => {
return createComponent({
state: {
id: 'gid/1',
name: 'state-1',
latestVersion: null,
},
});
});
it('does not display a download button', () => {
expect(findDownloadBtn().exists()).toBe(false);
});
});
});
import { GlIcon, GlTooltip } from '@gitlab/ui'; import { GlIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import StateActions from '~/terraform/components/states_table_actions.vue';
import StatesTable from '~/terraform/components/states_table.vue'; import StatesTable from '~/terraform/components/states_table.vue';
describe('StatesTable', () => { describe('StatesTable', () => {
let wrapper; let wrapper;
useFakeDate([2020, 10, 15]); useFakeDate([2020, 10, 15]);
const propsData = { const defaultProps = {
states: [ states: [
{ {
name: 'state-1', name: 'state-1',
...@@ -52,9 +53,15 @@ describe('StatesTable', () => { ...@@ -52,9 +53,15 @@ describe('StatesTable', () => {
], ],
}; };
beforeEach(() => { const createComponent = (propsData = defaultProps) => {
wrapper = mount(StatesTable, { propsData }); wrapper = mount(StatesTable, { propsData });
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
};
const findActions = () => wrapper.findAll(StateActions);
beforeEach(() => {
return createComponent();
}); });
afterEach(() => { afterEach(() => {
...@@ -99,4 +106,21 @@ describe('StatesTable', () => { ...@@ -99,4 +106,21 @@ describe('StatesTable', () => {
expect(state.text()).toMatchInterpolatedText(updateTime); expect(state.text()).toMatchInterpolatedText(updateTime);
}); });
it('displays no actions dropdown', () => {
expect(findActions().length).toEqual(0);
});
describe('when user is a terraform administrator', () => {
beforeEach(() => {
return createComponent({
terraformAdmin: true,
...defaultProps,
});
});
it('displays an actions dropdown for each state', () => {
expect(findActions().length).toEqual(defaultProps.states.length);
});
});
}); });
...@@ -5,10 +5,11 @@ require 'spec_helper' ...@@ -5,10 +5,11 @@ require 'spec_helper'
RSpec.describe Projects::TerraformHelper do RSpec.describe Projects::TerraformHelper do
describe '#js_terraform_list_data' do describe '#js_terraform_list_data' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:current_user) { project.creator }
subject { helper.js_terraform_list_data(project) } subject { helper.js_terraform_list_data(current_user, project) }
it 'displays image path' do it 'includes image path' do
image_path = ActionController::Base.helpers.image_path( image_path = ActionController::Base.helpers.image_path(
'illustrations/empty-state/empty-serverless-lg.svg' 'illustrations/empty-state/empty-serverless-lg.svg'
) )
...@@ -16,8 +17,28 @@ RSpec.describe Projects::TerraformHelper do ...@@ -16,8 +17,28 @@ RSpec.describe Projects::TerraformHelper do
expect(subject[:empty_state_image]).to eq(image_path) expect(subject[:empty_state_image]).to eq(image_path)
end end
it 'displays project path' do it 'includes project path' do
expect(subject[:project_path]).to eq(project.full_path) expect(subject[:project_path]).to eq(project.full_path)
end end
it 'indicates the user is a terraform admin' do
expect(subject[:terraform_admin]).to eq(true)
end
context 'when current_user is not a terraform admin' do
let(:current_user) { create(:user) }
it 'indicates the user is not an admin' do
expect(subject[:terraform_admin]).to eq(false)
end
end
context 'when current_user is missing' do
let(:current_user) { nil }
it 'indicates the user is not an admin' do
expect(subject[:terraform_admin]).to be_nil
end
end
end end
end end
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