Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cffcf0772c
commit
e66e16c73c
13 changed files with 221 additions and 8 deletions
|
@ -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>
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initExpiresAtField from '~/access_tokens';
|
||||
import { initExpiresAtField } from '~/access_tokens';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initExpiresAtField);
|
||||
initExpiresAtField();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import initExpiresAtField from '~/access_tokens';
|
||||
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initExpiresAtField);
|
||||
initExpiresAtField();
|
||||
initProjectsField();
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
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' }
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
62
spec/frontend/access_tokens/index_spec.js
Normal file
62
spec/frontend/access_tokens/index_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue