Commit 878f79d7 authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Nicolò Maria Mezzopera

Link to Create/Edit User List Pages

Adds the final links to the create and edit pages for user lists as well
as adds documentation for the feature.
parent a4dfe1f2
...@@ -2,13 +2,12 @@ ...@@ -2,13 +2,12 @@
import { createNamespacedHelpers } from 'vuex'; import { createNamespacedHelpers } from 'vuex';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
GlAlert,
GlButton, GlButton,
GlEmptyState, GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
GlModalDirective, GlModalDirective,
GlLink, GlLink,
GlAlert,
GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants'; import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from '../constants';
import FeatureFlagsTable from './feature_flags_table.vue'; import FeatureFlagsTable from './feature_flags_table.vue';
...@@ -34,12 +33,11 @@ export default { ...@@ -34,12 +33,11 @@ export default {
UserListsTable, UserListsTable,
NavigationTabs, NavigationTabs,
TablePagination, TablePagination,
GlAlert,
GlButton,
GlEmptyState, GlEmptyState,
GlLoadingIcon, GlLoadingIcon,
GlButton,
GlLink, GlLink,
GlAlert,
GlSprintf,
ConfigureFeatureFlagsModal, ConfigureFeatureFlagsModal,
}, },
directives: { directives: {
...@@ -74,10 +72,6 @@ export default { ...@@ -74,10 +72,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
userListsApiDocPath: {
type: String,
required: true,
},
rotateInstanceIdPath: { rotateInstanceIdPath: {
type: String, type: String,
required: false, required: false,
...@@ -100,6 +94,11 @@ export default { ...@@ -100,6 +94,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
newUserListPath: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { return {
...@@ -267,6 +266,18 @@ export default { ...@@ -267,6 +266,18 @@ export default {
> >
{{ s__('FeatureFlags|Configure') }} {{ s__('FeatureFlags|Configure') }}
</gl-button> </gl-button>
<gl-button
v-if="newUserListPath"
:href="newUserListPath"
variant="success"
category="secondary"
class="gl-mr-3"
data-testid="ff-new-list-button"
>
{{ s__('FeatureFlags|New list') }}
</gl-button>
<gl-button <gl-button
v-if="hasNewPath" v-if="hasNewPath"
:href="newFeatureFlagPath" :href="newFeatureFlagPath"
...@@ -277,20 +288,6 @@ export default { ...@@ -277,20 +288,6 @@ export default {
</gl-button> </gl-button>
</div> </div>
</div> </div>
<gl-alert v-if="!isUserListAlertDismissed" @dismiss="isUserListAlertDismissed = true">
<gl-sprintf
:message="
__('User Lists can only be created and modified with %{linkStart}the API%{linkEnd}')
"
>
<template #link="{ content }">
<gl-link :href="userListsApiDocPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert <gl-alert
v-for="(message, index) in alerts" v-for="(message, index) in alerts"
:key="index" :key="index"
......
...@@ -29,7 +29,7 @@ export default () => ...@@ -29,7 +29,7 @@ export default () =>
canUserConfigure: this.dataset.canUserAdminFeatureFlag, canUserConfigure: this.dataset.canUserAdminFeatureFlag,
newFeatureFlagPath: this.dataset.newFeatureFlagPath, newFeatureFlagPath: this.dataset.newFeatureFlagPath,
rotateInstanceIdPath: this.dataset.rotateInstanceIdPath, rotateInstanceIdPath: this.dataset.rotateInstanceIdPath,
userListsApiDocPath: this.dataset.userListsApiDocPath, newUserListPath: this.dataset.newUserListPath,
}, },
}); });
}, },
......
...@@ -39,6 +39,7 @@ export default { ...@@ -39,6 +39,7 @@ export default {
userIdLabel: s__('UserLists|User IDs'), userIdLabel: s__('UserLists|User IDs'),
userIdColumnHeader: s__('UserLists|User ID'), userIdColumnHeader: s__('UserLists|User ID'),
errorMessage: __('Something went wrong on our end. Please try again!'), errorMessage: __('Something went wrong on our end. Please try again!'),
editButtonLabel: s__('UserLists|Edit'),
}, },
classes: { classes: {
headerClasses: [ headerClasses: [
...@@ -72,6 +73,9 @@ export default { ...@@ -72,6 +73,9 @@ export default {
hasError() { hasError() {
return this.state === states.ERROR; return this.state === states.ERROR;
}, },
editPath() {
return this.userList?.edit_path;
},
}, },
mounted() { mounted() {
this.fetchUserList(); this.fetchUserList();
...@@ -86,7 +90,7 @@ export default { ...@@ -86,7 +90,7 @@ export default {
<gl-alert v-if="hasError" variant="danger" @dismiss="dismissErrorAlert"> <gl-alert v-if="hasError" variant="danger" @dismiss="dismissErrorAlert">
{{ $options.translations.errorMessage }} {{ $options.translations.errorMessage }}
</gl-alert> </gl-alert>
<gl-loading-icon v-if="isLoading" size="xl" class="mt-5" /> <gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-6" />
<div v-else> <div v-else>
<add-user-modal @addUsers="addUserIds" /> <add-user-modal @addUsers="addUserIds" />
<div :class="$options.classes.headerClasses"> <div :class="$options.classes.headerClasses">
...@@ -94,7 +98,10 @@ export default { ...@@ -94,7 +98,10 @@ export default {
<h3>{{ name }}</h3> <h3>{{ name }}</h3>
<h4 class="gl-text-gray-700">{{ $options.translations.userIdLabel }}</h4> <h4 class="gl-text-gray-700">{{ $options.translations.userIdLabel }}</h4>
</div> </div>
<div class="mt-5"> <div class="gl-mt-6">
<gl-button v-if="editPath" :href="editPath" data-testid="edit-user-list" class="gl-mr-3">
{{ $options.translations.editButtonLabel }}
</gl-button>
<gl-button <gl-button
v-gl-modal="$options.ADD_USER_MODAL_ID" v-gl-modal="$options.ADD_USER_MODAL_ID"
data-testid="add-users" data-testid="add-users"
......
# frozen_string_literal: true # frozen_string_literal: true
class Projects::FeatureFlagsUserListsController < Projects::ApplicationController class Projects::FeatureFlagsUserListsController < Projects::ApplicationController
before_action :check_feature_flag!, only: [:new, :edit]
before_action :authorize_admin_feature_flags_user_lists! before_action :authorize_admin_feature_flags_user_lists!
before_action :user_list, only: [:edit, :show] before_action :user_list, only: [:edit, :show]
...@@ -16,10 +15,6 @@ class Projects::FeatureFlagsUserListsController < Projects::ApplicationControlle ...@@ -16,10 +15,6 @@ class Projects::FeatureFlagsUserListsController < Projects::ApplicationControlle
private private
def check_feature_flag!
not_found unless Feature.enabled?(:feature_flag_user_lists, project)
end
def user_list def user_list
@user_list = project.operations_feature_flags_user_lists.find_by_iid!(params[:iid]) @user_list = project.operations_feature_flags_user_lists.find_by_iid!(params[:iid])
end end
......
...@@ -11,4 +11,4 @@ ...@@ -11,4 +11,4 @@
"can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project), "can-user-admin-feature-flag" => can?(current_user, :admin_feature_flag, @project),
"new-feature-flag-path" => can?(current_user, :create_feature_flag, @project) ? new_project_feature_flag_path(@project): nil, "new-feature-flag-path" => can?(current_user, :create_feature_flag, @project) ? new_project_feature_flag_path(@project): nil,
"rotate-instance-id-path" => can?(current_user, :admin_feature_flags_client, @project) ? reset_token_project_feature_flags_client_path(@project, format: :json) : nil, "rotate-instance-id-path" => can?(current_user, :admin_feature_flags_client, @project) ? reset_token_project_feature_flags_client_path(@project, format: :json) : nil,
"user-lists-api-doc-path" => help_page_path("api/feature_flag_user_lists.md") } } "new-user-list-path" => can?(current_user, :admin_feature_flags_user_lists, @project) ? new_project_feature_flags_user_list_path(@project) : nil } }
---
title: Allow Users to Create and Rename User Lists
merge_request: 36598
author:
type: added
...@@ -18,6 +18,10 @@ module EE ...@@ -18,6 +18,10 @@ module EE
expose :path do |list| expose :path do |list|
project_feature_flags_user_list_path(list.project, list) project_feature_flags_user_list_path(list.project, list)
end end
expose :edit_path do |list|
edit_project_feature_flags_user_list_path(list.project, list)
end
end end
end end
end end
......
...@@ -60,15 +60,6 @@ RSpec.describe Projects::FeatureFlagsUserListsController do ...@@ -60,15 +60,6 @@ RSpec.describe Projects::FeatureFlagsUserListsController do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it 'returns not found when the feature flag is off' do
stub_feature_flags(feature_flag_user_lists: false)
sign_in(developer)
get(:new, params: request_params)
expect(response).to have_gitlab_http_status(:not_found)
end
end end
describe 'GET #edit' do describe 'GET #edit' do
...@@ -100,15 +91,6 @@ RSpec.describe Projects::FeatureFlagsUserListsController do ...@@ -100,15 +91,6 @@ RSpec.describe Projects::FeatureFlagsUserListsController do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'returns not found when the feature flag is off' do
stub_feature_flags(feature_flag_user_lists: false)
list = create(:operations_feature_flag_user_list, project: project)
get(:edit, params: request_params(iid: list.iid))
expect(response).to have_gitlab_http_status(:not_found)
end
end end
describe 'GET #show' do describe 'GET #show' do
......
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { GlEmptyState, GlLoadingIcon, GlAlert } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import Api from 'ee/api'; import Api from 'ee/api';
import store from 'ee/feature_flags/store'; import store from 'ee/feature_flags/store';
import FeatureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue'; import FeatureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue';
...@@ -9,7 +9,6 @@ import UserListsTable from 'ee/feature_flags/components/user_lists_table.vue'; ...@@ -9,7 +9,6 @@ import UserListsTable from 'ee/feature_flags/components/user_lists_table.vue';
import ConfigureFeatureFlagsModal from 'ee/feature_flags/components/configure_feature_flags_modal.vue'; import ConfigureFeatureFlagsModal from 'ee/feature_flags/components/configure_feature_flags_modal.vue';
import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from 'ee/feature_flags/constants'; import { FEATURE_FLAG_SCOPE, USER_LIST_SCOPE } from 'ee/feature_flags/constants';
import { TEST_HOST } from 'spec/test_constants'; import { TEST_HOST } from 'spec/test_constants';
import { trimText } from 'helpers/text_helper';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -23,12 +22,12 @@ describe('Feature flags', () => { ...@@ -23,12 +22,12 @@ describe('Feature flags', () => {
featureFlagsHelpPagePath: '/help/feature-flags', featureFlagsHelpPagePath: '/help/feature-flags',
featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients', featureFlagsClientLibrariesHelpPagePath: '/help/feature-flags#unleash-clients',
featureFlagsClientExampleHelpPagePath: '/help/feature-flags#client-example', featureFlagsClientExampleHelpPagePath: '/help/feature-flags#client-example',
userListsApiDocPath: '/help/api/user_lists',
unleashApiUrl: `${TEST_HOST}/api/unleash`, unleashApiUrl: `${TEST_HOST}/api/unleash`,
unleashApiInstanceId: 'oP6sCNRqtRHmpy1gw2-F', unleashApiInstanceId: 'oP6sCNRqtRHmpy1gw2-F',
canUserConfigure: true, canUserConfigure: true,
canUserRotateToken: true, canUserRotateToken: true,
newFeatureFlagPath: 'feature-flags/new', newFeatureFlagPath: 'feature-flags/new',
newUserListPath: '/user-list/new',
projectId: '8', projectId: '8',
}; };
...@@ -43,6 +42,7 @@ describe('Feature flags', () => { ...@@ -43,6 +42,7 @@ describe('Feature flags', () => {
const configureButton = () => wrapper.find('[data-testid="ff-configure-button"]'); const configureButton = () => wrapper.find('[data-testid="ff-configure-button"]');
const newButton = () => wrapper.find('[data-testid="ff-new-button"]'); const newButton = () => wrapper.find('[data-testid="ff-new-button"]');
const newUserListButton = () => wrapper.find('[data-testid="ff-new-list-button"]');
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -65,30 +65,6 @@ describe('Feature flags', () => { ...@@ -65,30 +65,6 @@ describe('Feature flags', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('user lists alert', () => {
let alert;
beforeEach(async () => {
factory(mockData, mount);
await wrapper.vm.$nextTick();
alert = wrapper.find(GlAlert);
});
it('should show that user lists can only be modified by the API', () => {
expect(trimText(alert.text())).toContain(
'User Lists can only be created and modified with the API',
);
});
it('should be dismissible', async () => {
alert.find('button').trigger('click');
await wrapper.vm.$nextTick();
expect(alert.exists()).toBe(false);
});
});
describe('without permissions', () => { describe('without permissions', () => {
const propsData = { const propsData = {
endpoint: `${TEST_HOST}/endpoint.json`, endpoint: `${TEST_HOST}/endpoint.json`,
...@@ -102,7 +78,6 @@ describe('Feature flags', () => { ...@@ -102,7 +78,6 @@ describe('Feature flags', () => {
unleashApiUrl: `${TEST_HOST}/api/unleash`, unleashApiUrl: `${TEST_HOST}/api/unleash`,
unleashApiInstanceId: 'oP6sCNRqtRHmpy1gw2-F', unleashApiInstanceId: 'oP6sCNRqtRHmpy1gw2-F',
projectId: '8', projectId: '8',
userListsApiDocPath: '/help/api/user_lists',
}; };
beforeEach(done => { beforeEach(done => {
...@@ -124,6 +99,10 @@ describe('Feature flags', () => { ...@@ -124,6 +99,10 @@ describe('Feature flags', () => {
it('does not render new feature flag button', () => { it('does not render new feature flag button', () => {
expect(newButton().exists()).toBe(false); expect(newButton().exists()).toBe(false);
}); });
it('does not render new user list button', () => {
expect(newUserListButton().exists()).toBe(false);
});
}); });
describe('loading state', () => { describe('loading state', () => {
...@@ -181,6 +160,11 @@ describe('Feature flags', () => { ...@@ -181,6 +160,11 @@ describe('Feature flags', () => {
expect(newButton().exists()).toBe(true); expect(newButton().exists()).toBe(true);
}); });
it('renders new user list button', () => {
expect(newUserListButton().exists()).toBe(true);
expect(newUserListButton().attributes('href')).toBe('/user-list/new');
});
describe('in feature flags tab', () => { describe('in feature flags tab', () => {
it('renders generic title', () => { it('renders generic title', () => {
expect(emptyState.props('title')).toEqual('Get started with feature flags'); expect(emptyState.props('title')).toEqual('Get started with feature flags');
...@@ -237,6 +221,11 @@ describe('Feature flags', () => { ...@@ -237,6 +221,11 @@ describe('Feature flags', () => {
expect(newButton().exists()).toBe(true); expect(newButton().exists()).toBe(true);
}); });
it('renders new user list button', () => {
expect(newUserListButton().exists()).toBe(true);
expect(newUserListButton().attributes('href')).toBe('/user-list/new');
});
describe('pagination', () => { describe('pagination', () => {
it('should render pagination', () => { it('should render pagination', () => {
expect(wrapper.find(TablePagination).exists()).toBe(true); expect(wrapper.find(TablePagination).exists()).toBe(true);
...@@ -316,6 +305,11 @@ describe('Feature flags', () => { ...@@ -316,6 +305,11 @@ describe('Feature flags', () => {
it('renders new feature flag button', () => { it('renders new feature flag button', () => {
expect(newButton().exists()).toBe(true); expect(newButton().exists()).toBe(true);
}); });
it('renders new user list button', () => {
expect(newUserListButton().exists()).toBe(true);
expect(newUserListButton().attributes('href')).toBe('/user-list/new');
});
}); });
describe('rotate instance id', () => { describe('rotate instance id', () => {
......
...@@ -99,6 +99,7 @@ export const userList = { ...@@ -99,6 +99,7 @@ export const userList = {
created_at: '2020-02-04T08:13:10.507Z', created_at: '2020-02-04T08:13:10.507Z',
updated_at: '2020-02-04T08:13:10.507Z', updated_at: '2020-02-04T08:13:10.507Z',
path: '/path/to/user/list', path: '/path/to/user/list',
edit_path: '/path/to/user/list/edit',
}; };
export const allUsersStrategy = { export const allUsersStrategy = {
......
...@@ -77,6 +77,10 @@ describe('User List', () => { ...@@ -77,6 +77,10 @@ describe('User List', () => {
expect(wrapper.find('[data-testid="add-users"]').text()).toBe('Add Users'); expect(wrapper.find('[data-testid="add-users"]').text()).toBe('Add Users');
}); });
it('shows an edit list button', () => {
expect(wrapper.find('[data-testid="edit-user-list"]').text()).toBe('Edit');
});
it('shows a row for every id', () => { it('shows a row for every id', () => {
expect(wrapper.findAll('[data-testid="user-id-row"]')).toHaveLength(userIds.length); expect(wrapper.findAll('[data-testid="user-id-row"]')).toHaveLength(userIds.length);
}); });
......
...@@ -59,7 +59,8 @@ RSpec.describe API::FeatureFlagsUserLists do ...@@ -59,7 +59,8 @@ RSpec.describe API::FeatureFlagsUserLists do
'updated_at' => user_list.updated_at.as_json, 'updated_at' => user_list.updated_at.as_json,
'name' => 'list_a', 'name' => 'list_a',
'user_xids' => 'user1', 'user_xids' => 'user1',
'path' => project_feature_flags_user_list_path(user_list.project, user_list) 'path' => project_feature_flags_user_list_path(user_list.project, user_list),
'edit_path' => edit_project_feature_flags_user_list_path(user_list.project, user_list)
}]) }])
end end
...@@ -110,7 +111,7 @@ RSpec.describe API::FeatureFlagsUserLists do ...@@ -110,7 +111,7 @@ RSpec.describe API::FeatureFlagsUserLists do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
it 'returns the feature flag' do it 'returns the user list' do
list = create_list(name: 'testers', user_xids: 'test1,test2') list = create_list(name: 'testers', user_xids: 'test1,test2')
get api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer) get api("/projects/#{project.id}/feature_flags_user_lists/#{list.iid}", developer)
...@@ -124,11 +125,12 @@ RSpec.describe API::FeatureFlagsUserLists do ...@@ -124,11 +125,12 @@ RSpec.describe API::FeatureFlagsUserLists do
'project_id' => project.id, 'project_id' => project.id,
'created_at' => list.created_at.as_json, 'created_at' => list.created_at.as_json,
'updated_at' => list.updated_at.as_json, 'updated_at' => list.updated_at.as_json,
'path' => project_feature_flags_user_list_path(list.project, list) 'path' => project_feature_flags_user_list_path(list.project, list),
'edit_path' => edit_project_feature_flags_user_list_path(list.project, list)
}) })
end end
it 'returns the correct feature flag identified by the iid' do it 'returns the correct user list identified by the iid' do
create_list(name: 'list_a', user_xids: 'test1') create_list(name: 'list_a', user_xids: 'test1')
list_b = create_list(name: 'list_b', user_xids: 'test2') list_b = create_list(name: 'list_b', user_xids: 'test2')
......
...@@ -10336,6 +10336,9 @@ msgstr "" ...@@ -10336,6 +10336,9 @@ msgstr ""
msgid "FeatureFlags|New feature flag" msgid "FeatureFlags|New feature flag"
msgstr "" msgstr ""
msgid "FeatureFlags|New list"
msgstr ""
msgid "FeatureFlags|Percent rollout (logged in users)" msgid "FeatureFlags|Percent rollout (logged in users)"
msgstr "" msgstr ""
...@@ -26223,9 +26226,6 @@ msgstr "" ...@@ -26223,9 +26226,6 @@ msgstr ""
msgid "User List" msgid "User List"
msgstr "" msgstr ""
msgid "User Lists can only be created and modified with %{linkStart}the API%{linkEnd}"
msgstr ""
msgid "User OAuth applications" msgid "User OAuth applications"
msgstr "" msgstr ""
...@@ -26295,6 +26295,9 @@ msgstr "" ...@@ -26295,6 +26295,9 @@ msgstr ""
msgid "UserLists|Define a set of users to be used within feature flag strategies" msgid "UserLists|Define a set of users to be used within feature flag strategies"
msgstr "" msgstr ""
msgid "UserLists|Edit"
msgstr ""
msgid "UserLists|Edit %{name}" msgid "UserLists|Edit %{name}"
msgstr "" msgstr ""
......
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