Commit 1507163d authored by Stan Hu's avatar Stan Hu

Merge branch...

Merge branch '22754-restrict-personal-access-tokens-to-specific-projects-fe-radio-buttons' into 'master'

Add projects field to personal access token form [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!54839
parents ae0d4796 fbc73df6
<script>
import { GlFormGroup, GlFormRadio, GlFormText } from '@gitlab/ui';
export default {
name: 'ProjectsField',
ALL_PROJECTS: 'ALL_PROJECTS',
SELECTED_PROJECTS: 'SELECTED_PROJECTS',
components: { GlFormGroup, GlFormRadio, GlFormText },
props: {
inputAttrs: {
type: Object,
required: true,
},
},
data() {
return {
selectedRadio: this.$options.ALL_PROJECTS,
};
},
};
</script>
<template>
<div>
<gl-form-group :label="__('Projects')" label-class="gl-pb-0!">
<gl-form-text class="gl-pb-3">{{
__('Set access permissions for this token.')
}}</gl-form-text>
<gl-form-radio v-model="selectedRadio" :value="$options.ALL_PROJECTS">{{
__('All projects')
}}</gl-form-radio>
<gl-form-radio v-model="selectedRadio" :value="$options.SELECTED_PROJECTS">{{
__('Selected projects')
}}</gl-form-radio>
<input :id="inputAttrs.id" type="hidden" :name="inputAttrs.name" />
</gl-form-group>
</div>
</template>
import Vue from 'vue';
import ExpiresAtField from './components/expires_at_field.vue';
const getInputAttrs = (el) => {
......@@ -11,7 +12,7 @@ const getInputAttrs = (el) => {
};
};
const initExpiresAtField = () => {
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
if (!el) {
......@@ -32,4 +33,29 @@ const initExpiresAtField = () => {
});
};
export default initExpiresAtField;
export const initProjectsField = () => {
const el = document.querySelector('.js-access-tokens-projects');
if (!el) {
return null;
}
const inputAttrs = getInputAttrs(el);
if (window.gon.features.personalAccessTokensScopedToProjects) {
const ProjectsField = () => import('./components/projects_field.vue');
return new Vue({
el,
render(h) {
return h(ProjectsField, {
props: {
inputAttrs,
},
});
},
});
}
return null;
};
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField } from '~/access_tokens';
document.addEventListener('DOMContentLoaded', initExpiresAtField);
initExpiresAtField();
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
document.addEventListener('DOMContentLoaded', initExpiresAtField);
initExpiresAtField();
initProjectsField();
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField } from '~/access_tokens';
initExpiresAtField();
......@@ -3,6 +3,10 @@
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
feature_category :authentication_and_authorization
before_action do
push_frontend_feature_flag(:personal_access_tokens_scoped_to_projects, current_user)
end
def index
set_index_vars
@personal_access_token = finder.build
......
......@@ -29,5 +29,9 @@
= f.label :scopes, _('Scopes'), class: 'label-bold'
= render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes
- if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user)
.js-access-tokens-projects
%input{ type: 'hidden', name: 'temporary-name', id: 'temporary-id' }
.gl-mt-3
= f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-success', data: { qa_selector: 'create_token_button' }
---
name: personal_access_tokens_scoped_to_projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54617
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322187
milestone: '13.10'
type: development
group: group::access
default_enabled: false
......@@ -26812,6 +26812,9 @@ msgstr ""
msgid "Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users."
msgstr ""
msgid "Selected projects"
msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By %{link_open}@johnsmith%{link_close}\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr ""
......@@ -27031,6 +27034,9 @@ msgstr ""
msgid "Set a template repository for projects in this group"
msgstr ""
msgid "Set access permissions for this token."
msgstr ""
msgid "Set an instance-wide domain that will be available to all clusters when installing Knative."
msgstr ""
......
......@@ -138,4 +138,10 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
end
end
end
it 'pushes `personal_access_tokens_scoped_to_projects` feature flag to the frontend' do
visit profile_personal_access_tokens_path
expect(page).to have_pushed_frontend_feature_flags(personalAccessTokensScopedToProjects: true)
end
end
import { within } from '@testing-library/dom';
import { mount } from '@vue/test-utils';
import ProjectsField from '~/access_tokens/components/projects_field.vue';
describe('ProjectsField', () => {
let wrapper;
const createComponent = () => {
wrapper = mount(ProjectsField, {
propsData: {
inputAttrs: {
id: 'projects',
name: 'projects',
},
},
});
};
const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text);
const queryByText = (text) => within(wrapper.element).queryByText(text);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders label and sub-label', () => {
expect(queryByText('Projects')).not.toBe(null);
expect(queryByText('Set access permissions for this token.')).not.toBe(null);
});
it('renders "All projects" radio selected by default', () => {
const allProjectsRadio = queryByLabelText('All projects');
expect(allProjectsRadio).not.toBe(null);
expect(allProjectsRadio.checked).toBe(true);
});
it('renders "Selected projects" radio unchecked by default', () => {
const selectedProjectsRadio = queryByLabelText('Selected projects');
expect(selectedProjectsRadio).not.toBe(null);
expect(selectedProjectsRadio.checked).toBe(false);
});
it('renders hidden input with correct `name` and `id` attributes', () => {
expect(wrapper.find('input[type="hidden"]').attributes()).toEqual(
expect.objectContaining({
id: 'projects',
name: 'projects',
}),
);
});
});
import { createWrapper } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
import ExpiresAtField from '~/access_tokens/components/expires_at_field.vue';
import ProjectsField from '~/access_tokens/components/projects_field.vue';
describe('access tokens', () => {
beforeEach(() => {
window.gon = { features: { personalAccessTokensScopedToProjects: true } };
});
afterEach(() => {
document.body.innerHTML = '';
window.gon = {};
});
describe.each`
initFunction | mountSelector | expectedComponent
${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField}
${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => {
describe('when mount element exists', () => {
beforeEach(() => {
const mountEl = document.createElement('div');
mountEl.classList.add(mountSelector);
const input = document.createElement('input');
input.setAttribute('name', 'foo-bar');
input.setAttribute('id', 'foo-bar');
input.setAttribute('placeholder', 'Foo bar');
mountEl.appendChild(input);
document.body.appendChild(mountEl);
});
it(`mounts component and sets \`inputAttrs\` prop`, async () => {
const wrapper = createWrapper(initFunction());
// Wait for dynamic imports to resolve
await waitForPromises();
const component = wrapper.findComponent(expectedComponent);
expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({
name: 'foo-bar',
id: 'foo-bar',
placeholder: 'Foo bar',
});
});
});
describe('when mount element does not exist', () => {
it('returns `null`', () => {
expect(initFunction()).toBe(null);
});
});
});
});
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