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,44 +4,76 @@ require 'spec_helper' ...@@ -4,44 +4,76 @@ 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
gitlab_sign_in(user)
end
context 'when user does not have any terraform states and visits index page' do
before do before do
visit project_terraform_index_path(project) gitlab_sign_in(admin)
end end
it 'sees an empty state' do context 'when user does not have any terraform states and visits the index page' do
expect(page).to have_content('Get started with Terraform') let(:empty_project) { create(:project) }
end
end
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
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 'displays a tab with states count' do it 'sees an empty state' do
expect(page).to have_content("States #{project.terraform_states.size}") expect(page).to have_content('Get started with Terraform')
end end
end
context 'when user has a terraform state' do
context 'when user visits the index page' do
before do
visit project_terraform_index_path(project)
end
it 'displays a tab with states count' do
expect(page).to have_content("States #{project.terraform_states.size}")
end
it 'displays a table with terraform states' do
expect(page).to have_selector(
'[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
)
end
it 'displays a table with terraform states' do it 'displays terraform information' do
expect(page).to have_content(terraform_state.name)
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( expect(page).to have_selector(
'[data-testid="terraform-states-table"] tbody tr', '[data-testid="terraform-states-table-name"]',
count: project.terraform_states.size count: project.terraform_states.size
) )
end
it 'displays terraform information' do expect(page).not_to have_selector('[data-testid*="terraform-state-actions"]')
expect(page).to have_content(terraform_state.name)
end 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