Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-25 03:10:50 +00:00
parent cffcf0772c
commit e66e16c73c
13 changed files with 221 additions and 8 deletions

View file

@ -0,0 +1,38 @@
<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>

View file

@ -1,4 +1,5 @@
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;
};

View file

@ -1,3 +1,3 @@
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField } from '~/access_tokens';
document.addEventListener('DOMContentLoaded', initExpiresAtField);
initExpiresAtField();

View file

@ -1,3 +1,4 @@
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
document.addEventListener('DOMContentLoaded', initExpiresAtField);
initExpiresAtField();
initProjectsField();

View file

@ -1,3 +1,3 @@
import initExpiresAtField from '~/access_tokens';
import { initExpiresAtField } from '~/access_tokens';
initExpiresAtField();

View file

@ -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

View file

@ -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' }

View file

@ -0,0 +1,8 @@
---
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

View file

@ -1,7 +1,7 @@
# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/secret_detection
#
# Configure the scanning tool through the environment variables.
# List of the variables: https://gitlab.com/gitlab-org/security-products/secret_detection#available-variables
# List of the variables: https://docs.gitlab.com/ee/user/application_security/secret_detection/#available-variables
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:

View file

@ -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 ""

View file

@ -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

View file

@ -0,0 +1,58 @@
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',
}),
);
});
});

View file

@ -0,0 +1,62 @@
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);
});
});
});
});