Commit ca6263b9 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'afontaine/remove-legacy-flags-frontend-edit' into 'master'

Remove Frontend to Edit Legacy Flags [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!64005
parents 6d55b31f 21c7e932
<script>
import { GlAlert, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import axios from '~/lib/utils/axios_utils';
import { sprintf, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { LEGACY_FLAG } from '../constants';
import FeatureFlagForm from './form.vue';
export default {
......@@ -15,59 +13,29 @@ export default {
FeatureFlagForm,
},
mixins: [glFeatureFlagMixin()],
inject: {
showUserCallout: {},
userCalloutId: {
default: '',
},
userCalloutsPath: {
default: '',
},
},
data() {
return {
userShouldSeeNewFlagAlert: this.showUserCallout,
};
},
translations: {
legacyReadOnlyFlagAlert: s__(
'FeatureFlags|GitLab is moving to a new way of managing feature flags. This feature flag is read-only, and it will be removed in 14.0. Please create a new feature flag.',
),
},
computed: {
...mapState([
'path',
'error',
'name',
'description',
'scopes',
'strategies',
'isLoading',
'hasError',
'iid',
'active',
'version',
]),
title() {
return this.iid
? `^${this.iid} ${this.name}`
: sprintf(s__('Edit %{name}'), { name: this.name });
},
deprecated() {
return this.version === LEGACY_FLAG;
},
},
created() {
return this.fetchFeatureFlag();
},
methods: {
...mapActions(['updateFeatureFlag', 'fetchFeatureFlag', 'toggleActive']),
dismissNewVersionFlagAlert() {
this.userShouldSeeNewFlagAlert = false;
axios.post(this.userCalloutsPath, {
feature_name: this.userCalloutId,
});
},
},
};
</script>
......@@ -76,9 +44,6 @@ export default {
<gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-7" />
<template v-else-if="!isLoading && !hasError">
<gl-alert v-if="deprecated" variant="warning" :dismissible="false" class="gl-my-5">{{
$options.translations.legacyReadOnlyFlagAlert
}}</gl-alert>
<div class="gl-display-flex gl-align-items-center gl-mb-4 gl-mt-4">
<gl-toggle
:value="active"
......@@ -100,12 +65,10 @@ export default {
<feature-flag-form
:name="name"
:description="description"
:scopes="scopes"
:strategies="strategies"
:cancel-path="path"
:submit-text="__('Save changes')"
:active="active"
:version="version"
@handleSubmit="(data) => updateFeatureFlag(data)"
/>
</template>
......
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import EditFeatureFlag from './components/edit_feature_flag.vue';
import createStore from './store/edit';
......@@ -16,9 +15,6 @@ export default () => {
environmentsEndpoint,
projectId,
featureFlagIssuesEndpoint,
userCalloutsPath,
userCalloutId,
showUserCallout,
} = el.dataset;
return new Vue({
......@@ -30,9 +26,6 @@ export default () => {
environmentsEndpoint,
projectId,
featureFlagIssuesEndpoint,
userCalloutsPath,
userCalloutId,
showUserCallout: parseBoolean(showUserCallout),
},
render(createElement) {
return createElement(EditFeatureFlag);
......
......@@ -2,8 +2,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { NEW_VERSION_FLAG } from '../../constants';
import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
import { mapStrategiesToRails } from '../helpers';
import * as types from './mutation_types';
/**
......@@ -19,12 +18,7 @@ export const updateFeatureFlag = ({ state, dispatch }, params) => {
dispatch('requestUpdateFeatureFlag');
axios
.put(
state.endpoint,
params.version === NEW_VERSION_FLAG
? mapStrategiesToRails(params)
: mapFromScopesViewModel(params),
)
.put(state.endpoint, mapStrategiesToRails(params))
.then(() => {
dispatch('receiveUpdateFeatureFlagSuccess');
visitUrl(state.path);
......
import { LEGACY_FLAG } from '../../constants';
import { mapToScopesViewModel, mapStrategiesToViewModel } from '../helpers';
import { mapStrategiesToViewModel } from '../helpers';
import * as types from './mutation_types';
export default {
......@@ -14,7 +14,6 @@ export default {
state.description = response.description;
state.iid = response.iid;
state.active = response.active;
state.scopes = mapToScopesViewModel(response.scopes);
state.strategies = mapStrategiesToViewModel(response.strategies);
state.version = response.version || LEGACY_FLAG;
},
......
import { isEmpty, uniqueId, isString } from 'lodash';
import {
ROLLOUT_STRATEGY_ALL_USERS,
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ROLLOUT_STRATEGY_GITLAB_USER_LIST,
INTERNAL_ID_PREFIX,
DEFAULT_PERCENT_ROLLOUT,
PERCENT_ROLLOUT_GROUP_ID,
fetchPercentageParams,
fetchUserIdParams,
LEGACY_FLAG,
} from '../constants';
/**
* Converts raw scope objects fetched from the API into an array of scope
* objects that is easier/nicer to bind to in Vue.
* @param {Array} scopesFromRails An array of scope objects fetched from the API
*/
export const mapToScopesViewModel = (scopesFromRails) =>
(scopesFromRails || []).map((s) => {
const percentStrategy = (s.strategies || []).find(
(strat) => strat.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
);
const rolloutPercentage = fetchPercentageParams(percentStrategy) || DEFAULT_PERCENT_ROLLOUT;
const userStrategy = (s.strategies || []).find(
(strat) => strat.name === ROLLOUT_STRATEGY_USER_ID,
);
const rolloutStrategy =
(percentStrategy && percentStrategy.name) ||
(userStrategy && userStrategy.name) ||
ROLLOUT_STRATEGY_ALL_USERS;
const rolloutUserIds = (fetchUserIdParams(userStrategy) || '')
.split(',')
.filter((id) => id)
.join(', ');
return {
id: s.id,
environmentScope: s.environment_scope,
active: Boolean(s.active),
canUpdate: Boolean(s.can_update),
protected: Boolean(s.protected),
rolloutStrategy,
rolloutPercentage,
rolloutUserIds,
// eslint-disable-next-line no-underscore-dangle
shouldBeDestroyed: Boolean(s._destroy),
shouldIncludeUserIds: rolloutUserIds.length > 0 && percentStrategy !== null,
};
});
/**
* Converts the parameters emitted by the Vue component into
* the shape that the Rails API expects.
* @param {Array} scopesFromVue An array of scope objects from the Vue component
*/
export const mapFromScopesViewModel = (params) => {
const scopes = (params.scopes || []).map((s) => {
const parameters = {};
if (s.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT) {
parameters.groupId = PERCENT_ROLLOUT_GROUP_ID;
parameters.percentage = s.rolloutPercentage;
} else if (s.rolloutStrategy === ROLLOUT_STRATEGY_USER_ID) {
parameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ',');
}
const userIdParameters = {};
if (s.shouldIncludeUserIds && s.rolloutStrategy !== ROLLOUT_STRATEGY_USER_ID) {
userIdParameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ',');
}
// Strip out any internal IDs
const id = isString(s.id) && s.id.startsWith(INTERNAL_ID_PREFIX) ? undefined : s.id;
const strategies = [
{
name: s.rolloutStrategy,
parameters,
},
];
if (!isEmpty(userIdParameters)) {
strategies.push({ name: ROLLOUT_STRATEGY_USER_ID, parameters: userIdParameters });
}
return {
id,
environment_scope: s.environmentScope,
active: s.active,
can_update: s.canUpdate,
protected: s.protected,
_destroy: s.shouldBeDestroyed,
strategies,
};
});
const model = {
operations_feature_flag: {
name: params.name,
description: params.description,
active: params.active,
scopes_attributes: scopes,
version: LEGACY_FLAG,
},
};
return model;
};
/**
* Creates a new feature flag environment scope object for use
* in a Vue component. An optional parameter can be passed to
* override the property values that are created by default.
*
* @param {Object} overrides An optional object whose
* property values will be used to override the default values.
*
*/
export const createNewEnvironmentScope = (overrides = {}, featureFlagPermissions = false) => {
const defaultScope = {
environmentScope: '',
active: false,
id: uniqueId(INTERNAL_ID_PREFIX),
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
rolloutUserIds: '',
};
const newScope = {
...defaultScope,
...overrides,
};
if (featureFlagPermissions) {
newScope.canUpdate = true;
newScope.protected = false;
}
return newScope;
};
import { ROLLOUT_STRATEGY_GITLAB_USER_LIST, NEW_VERSION_FLAG } from '../constants';
const mapStrategyScopesToRails = (scopes) =>
scopes.length === 0
......@@ -206,8 +61,8 @@ export const mapStrategiesToRails = (params) => ({
operations_feature_flag: {
name: params.name,
description: params.description,
version: params.version,
active: params.active,
strategies_attributes: (params.strategies || []).map(mapStrategyToRails),
version: NEW_VERSION_FLAG,
},
});
......@@ -13,10 +13,6 @@ class Projects::FeatureFlagsController < Projects::ApplicationController
before_action :ensure_flag_writable!, only: [:update]
before_action :exclude_legacy_flags_check, only: [:edit]
before_action do
push_frontend_feature_flag(:feature_flag_permissions)
end
feature_category :feature_flags
def index
......
......@@ -8,9 +8,6 @@
project_id: @project.id,
feature_flags_path: project_feature_flags_path(@project),
environments_endpoint: search_project_environments_path(@project, format: :json),
user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::FEATURE_FLAGS_NEW_VERSION,
show_user_callout: show_feature_flags_new_version?.to_s,
strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scoping-environments-with-specs'),
feature_flag_issues_endpoint: feature_flag_issues_links_endpoint(@project, @feature_flag, current_user) } }
---
name: feature_flag_permissions
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/10096
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/254981
milestone: '11.10'
type: development
group: group::release
default_enabled: false
......@@ -13,9 +13,6 @@
.form-group
= f.label :deploy_access_levels_attributes, class: 'label-bold' do
= s_('ProtectedEnvironment|Allowed to deploy')
- if Feature.enabled?(:feature_flag_permissions, @project)
.text-muted.mb-2
= s_('ProtectedEnvironment|Select users to deploy and manage Feature Flag settings')
= render partial: 'projects/protected_environments/deploy_access_levels_dropdown', locals: { f: f }
.card-footer
......
......@@ -10,7 +10,6 @@ RSpec.describe 'User creates feature flag', :js do
before do
project.add_developer(user)
stub_feature_flags(feature_flag_permissions: false)
sign_in(user)
end
......
......@@ -15,7 +15,6 @@ RSpec.describe 'User deletes feature flag', :js do
before do
project.add_developer(user)
stub_feature_flags(feature_flag_permissions: false)
sign_in(user)
visit(project_feature_flags_path(project))
......
......@@ -13664,9 +13664,6 @@ msgstr ""
msgid "FeatureFlags|Get started with feature flags"
msgstr ""
msgid "FeatureFlags|GitLab is moving to a new way of managing feature flags. This feature flag is read-only, and it will be removed in 14.0. Please create a new feature flag."
msgstr ""
msgid "FeatureFlags|ID"
msgstr ""
......@@ -26423,9 +26420,6 @@ msgstr ""
msgid "ProtectedEnvironment|Select users"
msgstr ""
msgid "ProtectedEnvironment|Select users to deploy and manage Feature Flag settings"
msgstr ""
msgid "ProtectedEnvironment|There are currently no protected environments. Protect an environment with this form."
msgstr ""
......
......@@ -10,7 +10,6 @@ RSpec.describe 'User creates feature flag', :js do
before do
project.add_developer(user)
stub_feature_flags(feature_flag_permissions: false)
sign_in(user)
end
......
......@@ -15,7 +15,6 @@ RSpec.describe 'User deletes feature flag', :js do
before do
project.add_developer(user)
stub_feature_flags(feature_flag_permissions: false)
sign_in(user)
visit(project_feature_flags_path(project))
......
......@@ -13,9 +13,6 @@ RSpec.describe 'User updates feature flag', :js do
end
before do
stub_feature_flags(
feature_flag_permissions: false
)
sign_in(user)
end
......
import { GlToggle, GlAlert } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { TEST_HOST } from 'spec/test_constants';
import EditFeatureFlag from '~/feature_flags/components/edit_feature_flag.vue';
import Form from '~/feature_flags/components/form.vue';
import { LEGACY_FLAG, NEW_VERSION_FLAG } from '~/feature_flags/constants';
import createStore from '~/feature_flags/store/edit';
import axios from '~/lib/utils/axios_utils';
const localVue = createLocalVue();
localVue.use(Vuex);
const userCalloutId = 'feature_flags_new_version';
const userCalloutsPath = `${TEST_HOST}/user_callouts`;
Vue.use(Vuex);
describe('Edit feature flag form', () => {
let wrapper;
let mock;
......@@ -25,20 +20,14 @@ describe('Edit feature flag form', () => {
endpoint: `${TEST_HOST}/feature_flags.json`,
});
const factory = (opts = {}) => {
const factory = (provide = {}) => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
wrapper = shallowMount(EditFeatureFlag, {
localVue,
store,
provide: {
showUserCallout: true,
userCalloutId,
userCalloutsPath,
...opts,
},
provide,
});
};
......@@ -52,18 +41,8 @@ describe('Edit feature flag form', () => {
updated_at: '2019-01-17T17:27:39.778Z',
name: 'feature_flag',
description: '',
version: LEGACY_FLAG,
edit_path: '/h5bp/html5-boilerplate/-/feature_flags/21/edit',
destroy_path: '/h5bp/html5-boilerplate/-/feature_flags/21',
scopes: [
{
id: 21,
active: false,
environment_scope: '*',
created_at: '2019-01-17T17:27:39.778Z',
updated_at: '2019-01-17T17:27:39.778Z',
},
],
});
factory();
setImmediate(() => done());
......@@ -74,9 +53,7 @@ describe('Edit feature flag form', () => {
mock.restore();
});
const findAlert = () => wrapper.find(GlAlert);
const findWarningGlAlert = () =>
wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning');
const findWarningGlAlert = () => wrapper.findComponent(GlAlert);
it('should display the iid', () => {
expect(wrapper.find('h3').text()).toContain('^5');
......@@ -86,21 +63,13 @@ describe('Edit feature flag form', () => {
expect(wrapper.find(GlToggle).exists()).toBe(true);
});
it('should set the value of the toggle to whether or not the flag is active', () => {
expect(wrapper.find(GlToggle).props('value')).toBe(true);
});
it('should alert users the flag is read-only', () => {
expect(findAlert().text()).toContain('GitLab is moving to a new way of managing feature flags');
});
describe('with error', () => {
it('should render the error', () => {
store.dispatch('receiveUpdateFeatureFlagError', { message: ['The name is required'] });
return wrapper.vm.$nextTick(() => {
const warningGlAlert = findWarningGlAlert();
expect(warningGlAlert.at(1).exists()).toEqual(true);
expect(warningGlAlert.at(1).text()).toContain('The name is required');
expect(warningGlAlert.exists()).toEqual(true);
expect(warningGlAlert.text()).toContain('The name is required');
});
});
});
......@@ -114,32 +83,6 @@ describe('Edit feature flag form', () => {
expect(wrapper.find(Form).exists()).toEqual(true);
});
it('should set the version of the form from the feature flag', () => {
expect(wrapper.find(Form).attributes('version')).toBe(LEGACY_FLAG);
mock.resetHandlers();
mock.onGet(`${TEST_HOST}/feature_flags.json`).replyOnce(200, {
id: 21,
iid: 5,
active: true,
created_at: '2019-01-17T17:27:39.778Z',
updated_at: '2019-01-17T17:27:39.778Z',
name: 'feature_flag',
description: '',
version: NEW_VERSION_FLAG,
edit_path: '/h5bp/html5-boilerplate/-/feature_flags/21/edit',
destroy_path: '/h5bp/html5-boilerplate/-/feature_flags/21',
strategies: [],
});
factory();
return axios.waitForAll().then(() => {
expect(wrapper.find(Form).attributes('version')).toBe(NEW_VERSION_FLAG);
});
});
it('should track when the toggle is clicked', () => {
const toggle = wrapper.find(GlToggle);
const spy = mockTracking('_category_', toggle.element, jest.spyOn);
......
......@@ -16,86 +16,24 @@ export const featureFlag = {
destroy_path: 'feature_flags/1',
update_path: 'feature_flags/1',
edit_path: 'feature_flags/1/edit',
scopes: [
strategies: [
{
id: 1,
active: true,
environment_scope: '*',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
],
id: 9,
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
scopes: [{ id: 17, environment_scope: '*' }],
},
{
id: 2,
active: false,
environment_scope: 'production',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
],
id: 8,
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: {},
scopes: [{ id: 18, environment_scope: 'review/*' }],
},
{
id: 3,
active: false,
environment_scope: 'review/*',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
parameters: {},
},
],
},
{
id: 4,
active: true,
environment_scope: 'development',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
parameters: {
percentage: '86',
},
},
],
},
{
id: 5,
active: true,
environment_scope: 'development',
can_update: true,
protected: false,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
strategies: [
{
name: ROLLOUT_STRATEGY_FLEXIBLE_ROLLOUT,
parameters: {
rollout: '42',
stickiness: 'DEFAULT',
},
},
],
id: 7,
name: ROLLOUT_STRATEGY_USER_ID,
parameters: { userIds: '1,2,3,4' },
scopes: [{ id: 19, environment_scope: 'production' }],
},
],
};
......
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import {
NEW_VERSION_FLAG,
LEGACY_FLAG,
ROLLOUT_STRATEGY_ALL_USERS,
} from '~/feature_flags/constants';
import { ROLLOUT_STRATEGY_ALL_USERS } from '~/feature_flags/constants';
import {
updateFeatureFlag,
requestUpdateFeatureFlag,
......@@ -19,7 +15,7 @@ import {
} from '~/feature_flags/store/edit/actions';
import * as types from '~/feature_flags/store/edit/mutation_types';
import state from '~/feature_flags/store/edit/state';
import { mapStrategiesToRails, mapFromScopesViewModel } from '~/feature_flags/store/helpers';
import { mapStrategiesToRails } from '~/feature_flags/store/helpers';
import axios from '~/lib/utils/axios_utils';
jest.mock('~/lib/utils/url_utility');
......@@ -45,47 +41,10 @@ describe('Feature flags Edit Module actions', () => {
describe('success', () => {
it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagSuccess ', (done) => {
const featureFlag = {
name: 'feature_flag',
description: 'feature flag',
scopes: [
{
id: '1',
environmentScope: '*',
active: true,
shouldBeDestroyed: false,
canUpdate: true,
protected: false,
rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
},
],
version: LEGACY_FLAG,
active: true,
};
mock.onPut(mockedState.endpoint, mapFromScopesViewModel(featureFlag)).replyOnce(200);
testAction(
updateFeatureFlag,
featureFlag,
mockedState,
[],
[
{
type: 'requestUpdateFeatureFlag',
},
{
type: 'receiveUpdateFeatureFlagSuccess',
},
],
done,
);
});
it('handles new version flags as well', (done) => {
const featureFlag = {
name: 'name',
description: 'description',
active: true,
version: NEW_VERSION_FLAG,
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
......
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { ROLLOUT_STRATEGY_ALL_USERS, NEW_VERSION_FLAG } from '~/feature_flags/constants';
import { ROLLOUT_STRATEGY_ALL_USERS } from '~/feature_flags/constants';
import { mapStrategiesToRails } from '~/feature_flags/store/helpers';
import {
createFeatureFlag,
requestCreateFeatureFlag,
......@@ -39,7 +38,6 @@ describe('Feature flags New Module Actions', () => {
name: 'name',
description: 'description',
active: true,
version: NEW_VERSION_FLAG,
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
......@@ -76,7 +74,6 @@ describe('Feature flags New Module Actions', () => {
name: 'name',
description: 'description',
active: true,
version: NEW_VERSION_FLAG,
strategies: [
{
name: ROLLOUT_STRATEGY_ALL_USERS,
......
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