Commit cf7fdfc9 authored by Tiger's avatar Tiger

Add environment scope to group variables UI

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56812
parent c36cf6d0
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
GlFormCombobox, GlFormCombobox,
GlFormGroup, GlFormGroup,
GlFormSelect, GlFormSelect,
GlFormInput,
GlFormTextarea, GlFormTextarea,
GlIcon, GlIcon,
GlLink, GlLink,
...@@ -41,6 +42,7 @@ export default { ...@@ -41,6 +42,7 @@ export default {
GlFormCombobox, GlFormCombobox,
GlFormGroup, GlFormGroup,
GlFormSelect, GlFormSelect,
GlFormInput,
GlFormTextarea, GlFormTextarea,
GlIcon, GlIcon,
GlLink, GlLink,
...@@ -128,6 +130,12 @@ export default { ...@@ -128,6 +130,12 @@ export default {
return true; return true;
}, },
scopedVariablesEnabled() {
return !this.isGroup || this.glFeatures.scopedGroupVariables;
},
scopedVariablesAvailable() {
return !this.isGroup || this.glFeatures.groupScopedCiVariables;
},
variableValidationFeedback() { variableValidationFeedback() {
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`; return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
}, },
...@@ -226,24 +234,27 @@ export default { ...@@ -226,24 +234,27 @@ export default {
:label="__('Type')" :label="__('Type')"
label-for="ci-variable-type" label-for="ci-variable-type"
class="w-50 gl-mr-5" class="w-50 gl-mr-5"
:class="{ 'w-100': isGroup }" :class="{ 'w-100': !scopedVariablesEnabled }"
> >
<gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" /> <gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
v-if="!isGroup" v-if="scopedVariablesEnabled"
:label="__('Environment scope')" :label="__('Environment scope')"
label-for="ci-variable-env" label-for="ci-variable-env"
class="w-50" class="w-50"
data-testid="environment-scope" data-testid="environment-scope"
> >
<ci-environments-dropdown <ci-environments-dropdown
v-if="scopedVariablesAvailable"
class="w-100" class="w-100"
:value="environment_scope" :value="environment_scope"
@selectEnvironment="setEnvironmentScope" @selectEnvironment="setEnvironmentScope"
@createClicked="addWildCardScope" @createClicked="addWildCardScope"
/> />
<gl-form-input v-else v-model="environment_scope" class="w-100" readonly />
</gl-form-group> </gl-form-group>
</div> </div>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui'; import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants'; import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import CiVariablePopover from './ci_variable_popover.vue'; import CiVariablePopover from './ci_variable_popover.vue';
...@@ -59,6 +60,7 @@ export default { ...@@ -59,6 +60,7 @@ export default {
directives: { directives: {
GlModalDirective, GlModalDirective,
}, },
mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...mapState(['variables', 'valuesHidden', 'isGroup', 'isLoading', 'isDeleting']), ...mapState(['variables', 'valuesHidden', 'isGroup', 'isLoading', 'isDeleting']),
valuesButtonText() { valuesButtonText() {
...@@ -68,7 +70,7 @@ export default { ...@@ -68,7 +70,7 @@ export default {
return this.variables && this.variables.length > 0; return this.variables && this.variables.length > 0;
}, },
fields() { fields() {
if (this.isGroup) { if (this.isGroup && !this.glFeatures.scopedGroupVariables) {
return this.$options.fields.filter((field) => field.key !== 'environment_scope'); return this.$options.fields.filter((field) => field.key !== 'environment_scope');
} }
return this.$options.fields; return this.$options.fields;
......
...@@ -10,6 +10,8 @@ module Groups ...@@ -10,6 +10,8 @@ module Groups
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update] before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action :define_variables, only: [:show] before_action :define_variables, only: [:show]
before_action :push_feature_flags, only: [:show]
before_action :push_licensed_features, only: [:show]
feature_category :continuous_integration feature_category :continuous_integration
...@@ -91,6 +93,16 @@ module Groups ...@@ -91,6 +93,16 @@ module Groups
def update_group_params def update_group_params
params.require(:group).permit(:max_artifacts_size) params.require(:group).permit(:max_artifacts_size)
end end
def push_feature_flags
push_frontend_feature_flag(:scoped_group_variables, group)
end
# Overridden in EE
def push_licensed_features
end
end end
end end
end end
Groups::Settings::CiCdController.prepend_if_ee('EE::Groups::Settings::CiCdController')
...@@ -56,3 +56,5 @@ module Groups ...@@ -56,3 +56,5 @@ module Groups
end end
end end
end end
Groups::VariablesController.prepend_if_ee('EE::Groups::VariablesController')
...@@ -86,7 +86,7 @@ class Group < Namespace ...@@ -86,7 +86,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_sub_groups validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent validate :visibility_level_allowed_by_parent
validate :two_factor_authentication_allowed validate :two_factor_authentication_allowed
validates :variables, nested_attributes_duplicates: true validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
......
...@@ -2,5 +2,6 @@ ...@@ -2,5 +2,6 @@
module Ci module Ci
class GroupVariableEntity < Ci::BasicVariableEntity class GroupVariableEntity < Ci::BasicVariableEntity
expose :environment_scope
end end
end end
# frozen_string_literal: true
module EE
module Groups
module Settings
module CiCdController
extend ::Gitlab::Utils::Override
override :push_licensed_features
def push_licensed_features
push_licensed_feature(:group_scoped_ci_variables, group)
end
end
end
end
end
# frozen_string_literal: true
module EE
module Groups
module VariablesController
extend ::Gitlab::Utils::Override
override :variable_params_attributes
def variable_params_attributes
if group.scoped_variables_available?
super << :environment_scope
else
super
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::VariablesController do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:variable) { create(:ci_group_variable, group: group, environment_scope: '*') }
before do
sign_in(user)
group.add_user(user, :owner)
end
describe 'PATCH #update' do
let(:params) do
{
group_id: group,
variables_attributes: [{
id: variable.id,
environment_scope: 'production'
}]
}
end
before do
stub_licensed_features(group_scoped_ci_variables: scoped_variables_available)
end
subject { patch :update, params: params, format: :json }
context 'scoped variables are available' do
let(:scoped_variables_available) { true }
it 'updates the environment scope' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(variable.reload.environment_scope).to eq('production')
end
end
context 'scoped variables are not available' do
let(:scoped_variables_available) { false }
it 'does not update the environment scope' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(variable.reload.environment_scope).to eq('*')
end
end
end
end
import { GlButton } from '@gitlab/ui'; import { GlButton, GlFormInput } from '@gitlab/ui';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants'; import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
import createStore from '~/ci_variable_list/store'; import createStore from '~/ci_variable_list/store';
...@@ -15,7 +16,7 @@ describe('Ci variable modal', () => { ...@@ -15,7 +16,7 @@ describe('Ci variable modal', () => {
let store; let store;
const createComponent = (method, options = {}) => { const createComponent = (method, options = {}) => {
store = createStore(); store = createStore({ isGroup: options.isGroup });
wrapper = method(CiVariableModal, { wrapper = method(CiVariableModal, {
attachTo: document.body, attachTo: document.body,
stubs: { stubs: {
...@@ -27,6 +28,7 @@ describe('Ci variable modal', () => { ...@@ -27,6 +28,7 @@ describe('Ci variable modal', () => {
}); });
}; };
const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown);
const findModal = () => wrapper.find(ModalStub); const findModal = () => wrapper.find(ModalStub);
const findAddorUpdateButton = () => const findAddorUpdateButton = () =>
findModal() findModal()
...@@ -149,6 +151,61 @@ describe('Ci variable modal', () => { ...@@ -149,6 +151,61 @@ describe('Ci variable modal', () => {
}); });
}); });
describe('Environment scope', () => {
describe('group level variables', () => {
it('renders the environment dropdown', () => {
createComponent(shallowMount, {
isGroup: true,
provide: {
glFeatures: {
scopedGroupVariables: true,
groupScopedCiVariables: true,
},
},
});
expect(findCiEnvironmentsDropdown().exists()).toBe(true);
expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
});
describe('feature flag is disabled', () => {
it('hides the dropdown', () => {
createComponent(shallowMount, {
isGroup: true,
provide: {
glFeatures: {
scopedGroupVariables: false,
groupScopedCiVariables: true,
},
},
});
expect(findCiEnvironmentsDropdown().exists()).toBe(false);
});
});
describe('licensed feature is not available', () => {
it('disables the dropdown', () => {
createComponent(mount, {
isGroup: true,
provide: {
glFeatures: {
scopedGroupVariables: true,
groupScopedCiVariables: false,
},
},
});
const environmentScopeInput = wrapper
.find('[data-testid="environment-scope"]')
.find(GlFormInput);
expect(findCiEnvironmentsDropdown().exists()).toBe(false);
expect(environmentScopeInput.attributes('readonly')).toBe('readonly');
});
});
});
});
describe('Validations', () => { describe('Validations', () => {
const maskError = 'This variable can not be masked.'; const maskError = 'This variable can not be masked.';
......
import { GlTable } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
...@@ -12,21 +11,24 @@ describe('Ci variable table', () => { ...@@ -12,21 +11,24 @@ describe('Ci variable table', () => {
let wrapper; let wrapper;
let store; let store;
const createComponent = () => { const createComponent = (isGroup = false, scopedGroupVariables = false) => {
store = createStore(); store = createStore({ isGroup });
store.state.isGroup = true;
jest.spyOn(store, 'dispatch').mockImplementation(); jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mount(CiVariableTable, { wrapper = mount(CiVariableTable, {
attachTo: document.body, attachTo: document.body,
localVue, localVue,
store, store,
provide: {
glFeatures: {
scopedGroupVariables,
},
},
}); });
}; };
const findRevealButton = () => wrapper.find({ ref: 'secret-value-reveal-button' }); const findRevealButton = () => wrapper.find({ ref: 'secret-value-reveal-button' });
const findEditButton = () => wrapper.find({ ref: 'edit-ci-variable' }); const findEditButton = () => wrapper.find({ ref: 'edit-ci-variable' });
const findEmptyVariablesPlaceholder = () => wrapper.find({ ref: 'empty-variables' }); const findEmptyVariablesPlaceholder = () => wrapper.find({ ref: 'empty-variables' });
const findTable = () => wrapper.find(GlTable);
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
...@@ -40,12 +42,14 @@ describe('Ci variable table', () => { ...@@ -40,12 +42,14 @@ describe('Ci variable table', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchVariables'); expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
}); });
it('fields prop does not contain environment_scope if group', () => { it('fields do not contain environment_scope if group level and feature is disabled', () => {
expect(findTable().props('fields')).not.toEqual( createComponent(true, false);
expect(wrapper.vm.fields).not.toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
key: 'environment_scope', key: 'environment_scope',
label: 'Environment Scope', label: 'Environments',
}), }),
]), ]),
); );
......
...@@ -10,7 +10,7 @@ RSpec.describe Ci::GroupVariableEntity do ...@@ -10,7 +10,7 @@ RSpec.describe Ci::GroupVariableEntity do
subject { entity.as_json } subject { entity.as_json }
it 'contains required fields' do it 'contains required fields' do
expect(subject).to include(:id, :key, :value, :protected, :variable_type) expect(subject).to include(:id, :key, :value, :protected, :variable_type, :environment_scope)
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