diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue index c1e862f6fa889ac53d6ad85cf87c0b92bb0bc0eb..2fdf8456615a0ef8fb632052faac54e115fa30e3 100644 --- a/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue +++ b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue @@ -3,7 +3,15 @@ import { GlDropdown, GlDropdownForm, GlDropdownItem, GlDropdownDivider, GlIcon } import { s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants'; +import { + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + I18N_REGISTER_INSTANCE_TYPE, + I18N_REGISTER_GROUP_TYPE, + I18N_REGISTER_PROJECT_TYPE, + I18N_REGISTER_RUNNER, +} from '../../constants'; import RegistrationToken from './registration_token.vue'; import RegistrationTokenResetDropdownItem from './registration_token_reset_dropdown_item.vue'; @@ -51,20 +59,23 @@ export default { this.glFeatures?.createRunnerWorkflowForNamespace ); }, - dropdownText() { - if (this.isDeprecated) { - return ''; - } + actionText() { switch (this.type) { case INSTANCE_TYPE: - return s__('Runners|Register an instance runner'); + return I18N_REGISTER_INSTANCE_TYPE; case GROUP_TYPE: - return s__('Runners|Register a group runner'); + return I18N_REGISTER_GROUP_TYPE; case PROJECT_TYPE: - return s__('Runners|Register a project runner'); + return I18N_REGISTER_PROJECT_TYPE; default: - return s__('Runners|Register a runner'); + return I18N_REGISTER_RUNNER; + } + }, + dropdownText() { + if (this.isDeprecated) { + return ''; } + return this.actionText; }, dropdownToggleClass() { if (this.isDeprecated) { @@ -109,6 +120,7 @@ export default { v-bind="$attrs" > <template v-if="isDeprecated" #button-content> + <span class="gl-sr-only">{{ actionText }}</span> <gl-icon name="ellipsis_v" /> </template> <gl-dropdown-form class="gl-p-4!"> diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 84b2ed010e610f8181f72627ddc5a5d785dcdfd8..1225c0d758365d7a1f3c40ddaeadd7db5c71ed00 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -71,6 +71,12 @@ export const I18N_STALE_NEVER_CONTACTED_TOOLTIP = s__( 'Runners|Runner is stale; it has never contacted this instance', ); +// Registration dropdown +export const I18N_REGISTER_INSTANCE_TYPE = s__('Runners|Register an instance runner'); +export const I18N_REGISTER_GROUP_TYPE = s__('Runners|Register a group runner'); +export const I18N_REGISTER_PROJECT_TYPE = s__('Runners|Register a project runner'); +export const I18N_REGISTER_RUNNER = s__('Runners|Register a runner'); + // Actions export const I18N_EDIT = __('Edit'); diff --git a/app/assets/javascripts/ci/runner/project_runners/register/index.js b/app/assets/javascripts/ci/runner/project_runners/register/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9986c93c91819733d3a3f7fd7263954a0e2ddde1 --- /dev/null +++ b/app/assets/javascripts/ci/runner/project_runners/register/index.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue'; +import { PROJECT_TYPE } from '~/ci/runner/constants'; + +Vue.use(VueApollo); + +export const initProjectRunnersRegistrationDropdown = ( + selector = '#js-project-runner-registration-dropdown', +) => { + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { registrationToken, projectId } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + provide: { + projectId, + }, + render(h) { + return h(RegistrationDropdown, { + props: { + registrationToken, + type: PROJECT_TYPE, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 9ec560154054129904d650255242387744123d7d..731b1373987b4a2a3b654d63a975f8a8cf9a4d77 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -12,6 +12,7 @@ import { initTokenAccess } from '~/token_access'; import { initCiSecureFiles } from '~/ci_secure_files'; import initDeployTokens from '~/deploy_tokens'; import { initProjectRunners } from '~/ci/runner/project_runners'; +import { initProjectRunnersRegistrationDropdown } from '~/ci/runner/project_runners/register'; // Initialize expandable settings panels initSettingsPanels(); @@ -42,6 +43,7 @@ initSettingsPipelinesTriggers(); initArtifactsSettings(); initProjectRunners(); +initProjectRunnersRegistrationDropdown(); initSharedRunnersToggle(); initRefSwitcherBadges(); initInstallRunner(); diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 28ae730eda583ff093382eaf32ff34bc9f31ae16..626587deb710da702a4b50d33b15e8f3c3a01384 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -15,6 +15,7 @@ class CiCdController < Projects::ApplicationController before_action do push_frontend_feature_flag(:ci_variables_pages, current_user) push_frontend_feature_flag(:ci_limit_environment_scope, @project) + push_frontend_feature_flag(:create_runner_workflow_for_namespace, @project.namespace) end helper_method :highlight_badge diff --git a/app/views/projects/runners/_project_runners.html.haml b/app/views/projects/runners/_project_runners.html.haml index 9b401711ec5a3d84e027836c263a6dfe261c6f24..1d4e45c71b5b7651411fb6684b87d6a24f45b094 100644 --- a/app/views/projects/runners/_project_runners.html.haml +++ b/app/views/projects/runners/_project_runners.html.haml @@ -7,6 +7,7 @@ - if can?(current_user, :create_runner, @project) = render Pajamas::ButtonComponent.new(href: new_project_runner_path(@project), variant: :confirm) do = s_('Runners|New project runner') + #js-project-runner-registration-dropdown{ data: { registration_token: @project.runners_token, project_id: @project.id } } - else = _('Please contact an admin to create runners.') = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer' diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index d82f9acdd074e878eae53852f5e1627b9c49e821..582535790bdd321e3b75b956bece6b3b06237dd6 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -32,15 +32,30 @@ end describe "runners registration" do - before do - stub_feature_flags(create_runner_workflow_for_admin: false) + context 'when create_runner_workflow_for_namespace is enabled' do + before do + stub_feature_flags(create_runner_workflow_for_admin: true) - visit admin_runners_path + visit admin_runners_path + end + + it_behaves_like "shows and resets runner registration token" do + let(:dropdown_text) { s_('Runners|Register an instance runner') } + let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token } + end end - it_behaves_like "shows and resets runner registration token" do - let(:dropdown_text) { s_('Runners|Register an instance runner') } - let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token } + context 'when create_runner_workflow_for_namespace is disabled' do + before do + stub_feature_flags(create_runner_workflow_for_admin: false) + + visit admin_runners_path + end + + it_behaves_like "shows and resets runner registration token" do + let(:dropdown_text) { s_('Runners|Register an instance runner') } + let(:registration_token) { Gitlab::CurrentSettings.runners_registration_token } + end end end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 54482d141c71164c71b2df21da8bdeab8e8c5fa4..2de95c21003caf798fe6a2aae3afaa47d8c6fd1a 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -21,19 +21,28 @@ project.add_maintainer(user) end - context 'when create_runner_workflow_for_namespace is enabled' do + context 'when create_runner_workflow_for_namespace is enabled', :js do before do stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace]) - end - it 'user can see a link with instructions on how to install GitLab Runner' do visit project_runners_path(project) + end + it 'user can see a link with instructions on how to install GitLab Runner' do expect(page).to have_link(s_('Runners|New project runner'), href: new_project_runner_path(project)) end - describe 'runner registration', :js do + it_behaves_like "shows and resets runner registration token" do + let(:dropdown_text) { s_('Runners|Register a project runner') } + let(:registration_token) { project.runners_token } + end + end + + context 'when user views new runner page' do + context 'when create_runner_workflow_for_namespace is enabled', :js do before do + stub_feature_flags(create_runner_workflow_for_namespace: [project.namespace]) + visit new_project_runner_path(project) end diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js index 9df7a974af38320b5634d4d1f2ea7076d9ba0867..e564cf49ca0d813a2a092210251ed1a982490626 100644 --- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js +++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js @@ -12,7 +12,14 @@ import RegistrationDropdown from '~/ci/runner/components/registration/registrati import RegistrationToken from '~/ci/runner/components/registration/registration_token.vue'; import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants'; +import { + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + I18N_REGISTER_INSTANCE_TYPE, + I18N_REGISTER_GROUP_TYPE, + I18N_REGISTER_PROJECT_TYPE, +} from '~/ci/runner/constants'; import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql'; import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql'; @@ -81,13 +88,13 @@ describe('RegistrationDropdown', () => { it.each` type | text - ${INSTANCE_TYPE} | ${s__('Runners|Register an instance runner')} - ${GROUP_TYPE} | ${s__('Runners|Register a group runner')} - ${PROJECT_TYPE} | ${s__('Runners|Register a project runner')} - `('Dropdown text for type $type is "$text"', () => { - createComponent({ props: { type: INSTANCE_TYPE } }, mountExtended); + ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE} + ${GROUP_TYPE} | ${I18N_REGISTER_GROUP_TYPE} + ${PROJECT_TYPE} | ${I18N_REGISTER_PROJECT_TYPE} + `('Dropdown text for type $type is "$text"', ({ type, text }) => { + createComponent({ props: { type } }, mountExtended); - expect(wrapper.text()).toContain('Register an instance runner'); + expect(wrapper.text()).toContain(text); }); it('Passes attributes to dropdown', () => { @@ -214,7 +221,7 @@ describe('RegistrationDropdown', () => { { createRunnerWorkflowForAdmin: true }, { createRunnerWorkflowForNamespace: true }, ])('When showing a "deprecated" warning', (glFeatures) => { - it('Passes deprecated variant props and attributes to dropdown', () => { + it('passes deprecated variant props and attributes to dropdown', () => { createComponent({ provide: { glFeatures }, }); @@ -230,6 +237,17 @@ describe('RegistrationDropdown', () => { }); }); + it.each` + type | text + ${INSTANCE_TYPE} | ${I18N_REGISTER_INSTANCE_TYPE} + ${GROUP_TYPE} | ${I18N_REGISTER_GROUP_TYPE} + ${PROJECT_TYPE} | ${I18N_REGISTER_PROJECT_TYPE} + `('dropdown text for type $type is "$text"', ({ type, text }) => { + createComponent({ props: { type } }, mountExtended); + + expect(wrapper.text()).toContain(text); + }); + it('shows warning text', () => { createComponent( { @@ -243,7 +261,7 @@ describe('RegistrationDropdown', () => { expect(text.exists()).toBe(true); }); - it('button shows only ellipsis icon', () => { + it('button shows ellipsis icon', () => { createComponent( { provide: { glFeatures }, @@ -251,7 +269,6 @@ describe('RegistrationDropdown', () => { mountExtended, ); - expect(findDropdownBtn().text()).toBe(''); expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v'); expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1); });