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 {
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormInput,
GlFormTextarea,
GlIcon,
GlLink,
......@@ -41,6 +42,7 @@ export default {
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormInput,
GlFormTextarea,
GlIcon,
GlLink,
......@@ -128,6 +130,12 @@ export default {
return true;
},
scopedVariablesEnabled() {
return !this.isGroup || this.glFeatures.scopedGroupVariables;
},
scopedVariablesAvailable() {
return !this.isGroup || this.glFeatures.groupScopedCiVariables;
},
variableValidationFeedback() {
return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
},
......@@ -226,24 +234,27 @@ export default {
:label="__('Type')"
label-for="ci-variable-type"
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-group>
<gl-form-group
v-if="!isGroup"
v-if="scopedVariablesEnabled"
:label="__('Environment scope')"
label-for="ci-variable-env"
class="w-50"
data-testid="environment-scope"
>
<ci-environments-dropdown
v-if="scopedVariablesAvailable"
class="w-100"
:value="environment_scope"
@selectEnvironment="setEnvironmentScope"
@createClicked="addWildCardScope"
/>
<gl-form-input v-else v-model="environment_scope" class="w-100" readonly />
</gl-form-group>
</div>
......
......@@ -2,6 +2,7 @@
import { GlTable, GlButton, GlModalDirective, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
import CiVariablePopover from './ci_variable_popover.vue';
......@@ -59,6 +60,7 @@ export default {
directives: {
GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
computed: {
...mapState(['variables', 'valuesHidden', 'isGroup', 'isLoading', 'isDeleting']),
valuesButtonText() {
......@@ -68,7 +70,7 @@ export default {
return this.variables && this.variables.length > 0;
},
fields() {
if (this.isGroup) {
if (this.isGroup && !this.glFeatures.scopedGroupVariables) {
return this.$options.fields.filter((field) => field.key !== 'environment_scope');
}
return this.$options.fields;
......
......@@ -10,6 +10,8 @@ module Groups
before_action :authorize_admin_group!
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action :define_variables, only: [:show]
before_action :push_feature_flags, only: [:show]
before_action :push_licensed_features, only: [:show]
feature_category :continuous_integration
......@@ -91,6 +93,16 @@ module Groups
def update_group_params
params.require(:group).permit(:max_artifacts_size)
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
Groups::Settings::CiCdController.prepend_if_ee('EE::Groups::Settings::CiCdController')
......@@ -56,3 +56,5 @@ module Groups
end
end
end
Groups::VariablesController.prepend_if_ee('EE::Groups::VariablesController')
......@@ -86,7 +86,7 @@ class Group < Namespace
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
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 }
......
......@@ -2,5 +2,6 @@
module Ci
class GroupVariableEntity < Ci::BasicVariableEntity
expose :environment_scope
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 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 { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
import createStore from '~/ci_variable_list/store';
......@@ -15,7 +16,7 @@ describe('Ci variable modal', () => {
let store;
const createComponent = (method, options = {}) => {
store = createStore();
store = createStore({ isGroup: options.isGroup });
wrapper = method(CiVariableModal, {
attachTo: document.body,
stubs: {
......@@ -27,6 +28,7 @@ describe('Ci variable modal', () => {
});
};
const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown);
const findModal = () => wrapper.find(ModalStub);
const findAddorUpdateButton = () =>
findModal()
......@@ -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', () => {
const maskError = 'This variable can not be masked.';
......
import { GlTable } from '@gitlab/ui';
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
......@@ -12,21 +11,24 @@ describe('Ci variable table', () => {
let wrapper;
let store;
const createComponent = () => {
store = createStore();
store.state.isGroup = true;
const createComponent = (isGroup = false, scopedGroupVariables = false) => {
store = createStore({ isGroup });
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = mount(CiVariableTable, {
attachTo: document.body,
localVue,
store,
provide: {
glFeatures: {
scopedGroupVariables,
},
},
});
};
const findRevealButton = () => wrapper.find({ ref: 'secret-value-reveal-button' });
const findEditButton = () => wrapper.find({ ref: 'edit-ci-variable' });
const findEmptyVariablesPlaceholder = () => wrapper.find({ ref: 'empty-variables' });
const findTable = () => wrapper.find(GlTable);
beforeEach(() => {
createComponent();
......@@ -40,12 +42,14 @@ describe('Ci variable table', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
});
it('fields prop does not contain environment_scope if group', () => {
expect(findTable().props('fields')).not.toEqual(
it('fields do not contain environment_scope if group level and feature is disabled', () => {
createComponent(true, false);
expect(wrapper.vm.fields).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'environment_scope',
label: 'Environment Scope',
label: 'Environments',
}),
]),
);
......
......@@ -10,7 +10,7 @@ RSpec.describe Ci::GroupVariableEntity do
subject { entity.as_json }
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
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