Commit 059bd0af authored by Mireya Andres's avatar Mireya Andres Committed by Miguel Rincon

Migrate button toggle for shared project runners to Vue

To remove ambiguity with the current UI for enabling/disabling shared
runners in project settings, this MR replaces the button with a toggle
element. This is also consistent with our usage of toggles in the other
runner settings.

To do this, the UI has been ported to Vue while the POST endpoint
`toggle_shared_runners` has been modified to return the response in
JSON format.

The changes are gated by the `:vueify_shared_runners_toggle` feature
flag.
parent a23bc839
...@@ -4,6 +4,7 @@ import registrySettingsApp from '~/registry/settings/registry_settings_bundle'; ...@@ -4,6 +4,7 @@ import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initVariableList from '~/ci_variable_list'; import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze'; import initDeployFreeze from '~/deploy_freeze';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers'; import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels // Initialize expandable settings panels
...@@ -32,4 +33,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -32,4 +33,8 @@ document.addEventListener('DOMContentLoaded', () => {
initDeployFreeze(); initDeployFreeze();
initSettingsPipelinesTriggers(); initSettingsPipelinesTriggers();
if (gon?.features?.vueifySharedRunnersToggle) {
initSharedRunnersToggle();
}
}); });
<script>
import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
const DEFAULT_ERROR_MESSAGE = __('An error occurred while updating the configuration.');
export default {
components: {
GlAlert,
GlToggle,
GlTooltip,
},
props: {
isDisabledAndUnoverridable: {
type: Boolean,
required: true,
},
isEnabled: {
type: Boolean,
required: true,
},
updatePath: {
type: String,
required: true,
},
},
data() {
return {
isLoading: false,
isSharedRunnerEnabled: false,
errorMessage: null,
};
},
created() {
this.isSharedRunnerEnabled = this.isEnabled;
},
methods: {
toggleSharedRunners() {
this.isLoading = true;
this.errorMessage = null;
axios
.post(this.updatePath)
.then(() => {
this.isLoading = false;
this.isSharedRunnerEnabled = !this.isSharedRunnerEnabled;
})
.catch(error => {
this.isLoading = false;
this.errorMessage = error.response?.data?.error || DEFAULT_ERROR_MESSAGE;
});
},
},
};
</script>
<template>
<div>
<section class="gl-mt-5">
<gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" :dismissible="false">
{{ errorMessage }}
</gl-alert>
<div ref="sharedRunnersToggle">
<gl-toggle
:disabled="isDisabledAndUnoverridable"
:is-loading="isLoading"
:label="__('Enable shared runners for this project')"
:value="isSharedRunnerEnabled"
data-testid="toggle-shared-runners"
@change="toggleSharedRunners"
/>
</div>
<gl-tooltip v-if="isDisabledAndUnoverridable" :target="() => $refs.sharedRunnersToggle">
{{ __('Shared runners are disabled on group level') }}
</gl-tooltip>
</section>
</div>
</template>
import Vue from 'vue';
import SharedRunnersToggle from '~/projects/settings/components/shared_runners_toggle.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
export default (containerId = 'toggle-shared-runners-form') => {
const containerEl = document.getElementById(containerId);
const { isDisabledAndUnoverridable, isEnabled, updatePath } = containerEl.dataset;
return new Vue({
el: containerEl,
render(createElement) {
return createElement(SharedRunnersToggle, {
props: {
isDisabledAndUnoverridable: parseBoolean(isDisabledAndUnoverridable),
isEnabled: parseBoolean(isEnabled),
updatePath,
},
});
},
});
};
...@@ -53,12 +53,23 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -53,12 +53,23 @@ class Projects::RunnersController < Projects::ApplicationController
def toggle_shared_runners def toggle_shared_runners
if !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable' if !project.shared_runners_enabled && project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
return redirect_to project_runners_path(@project), alert: _("Cannot enable shared runners because parent group does not allow it")
if Feature.enabled?(:vueify_shared_runners_toggle, @project)
render json: { error: _('Cannot enable shared runners because parent group does not allow it') }, status: :unauthorized
else
redirect_to project_runners_path(@project), alert: _('Cannot enable shared runners because parent group does not allow it')
end
return
end end
project.toggle!(:shared_runners_enabled) project.toggle!(:shared_runners_enabled)
redirect_to project_settings_ci_cd_path(@project, anchor: 'js-runners-settings') if Feature.enabled?(:vueify_shared_runners_toggle, @project)
render json: {}, status: :ok
else
redirect_to project_settings_ci_cd_path(@project, anchor: 'js-runners-settings')
end
end end
def toggle_group_runners def toggle_group_runners
......
...@@ -11,6 +11,7 @@ module Projects ...@@ -11,6 +11,7 @@ module Projects
before_action :define_variables before_action :define_variables
before_action do before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project) push_frontend_feature_flag(:ajax_new_deploy_token, @project)
push_frontend_feature_flag(:vueify_shared_runners_toggle, @project)
end end
helper_method :highlight_badge helper_method :highlight_badge
......
...@@ -49,6 +49,14 @@ module Ci ...@@ -49,6 +49,14 @@ module Ci
parent_shared_runners_availability: group.parent&.shared_runners_setting parent_shared_runners_availability: group.parent&.shared_runners_setting
} }
end end
def toggle_shared_runners_settings_data(project)
{
is_enabled: "#{project.shared_runners_enabled?}",
is_disabled_and_unoverridable: "#{project.group&.shared_runners_setting == 'disabled_and_unoverridable'}",
update_path: toggle_shared_runners_project_runners_path(project)
}
end
end end
end end
......
- isVueifySharedRunnersToggleEnabled = Feature.enabled?(:vueify_shared_runners_toggle, @project)
= render layout: 'shared/runners/shared_runners_description' do = render layout: 'shared/runners/shared_runners_description' do
%hr - if !isVueifySharedRunnersToggleEnabled
- if @project.group&.shared_runners_setting == 'disabled_and_unoverridable' %hr
%h5.gl-text-red-500 - if @project.group&.shared_runners_setting == 'disabled_and_unoverridable'
= _('Shared runners disabled on group level') %h5.gl-text-red-500
- else = _('Shared runners disabled on group level')
- if @project.shared_runners_enabled?
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
= _('Disable shared runners')
- else - else
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do - if @project.shared_runners_enabled?
= _('Enable shared runners') = link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
&nbsp; for this project = _('Disable shared runners')
- else
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do
= _('Enable shared runners')
&nbsp; for this project
- if isVueifySharedRunnersToggleEnabled
#toggle-shared-runners-form{ data: toggle_shared_runners_settings_data(@project) }
- if @shared_runners_count == 0 - if @shared_runners_count == 0
= _('This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area.') = _('This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area.')
......
---
name: vueify_shared_runners_toggle
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48452
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292441
milestone: '13.7'
type: development
group: group::continuous integration
default_enabled: false
...@@ -3312,6 +3312,9 @@ msgstr "" ...@@ -3312,6 +3312,9 @@ msgstr ""
msgid "An error occurred while updating the comment" msgid "An error occurred while updating the comment"
msgstr "" msgstr ""
msgid "An error occurred while updating the configuration."
msgstr ""
msgid "An error occurred while updating the milestone." msgid "An error occurred while updating the milestone."
msgstr "" msgstr ""
...@@ -10449,6 +10452,9 @@ msgstr "" ...@@ -10449,6 +10452,9 @@ msgstr ""
msgid "Enable shared runners for this group" msgid "Enable shared runners for this group"
msgstr "" msgstr ""
msgid "Enable shared runners for this project"
msgstr ""
msgid "Enable smartcn custom analyzer: Indexing" msgid "Enable smartcn custom analyzer: Indexing"
msgstr "" msgstr ""
...@@ -25255,6 +25261,9 @@ msgstr "" ...@@ -25255,6 +25261,9 @@ msgstr ""
msgid "Shared runners are disabled for the parent group" msgid "Shared runners are disabled for the parent group"
msgstr "" msgstr ""
msgid "Shared runners are disabled on group level"
msgstr ""
msgid "Shared runners disabled on group level" msgid "Shared runners disabled on group level"
msgstr "" msgstr ""
......
...@@ -78,40 +78,84 @@ RSpec.describe Projects::RunnersController do ...@@ -78,40 +78,84 @@ RSpec.describe Projects::RunnersController do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:project) { create(:project, group: group) } let(:project) { create(:project, group: group) }
it 'toggles shared_runners_enabled when the group allows shared runners' do context 'without feature flag' do
project.update!(shared_runners_enabled: true) before do
stub_feature_flags(vueify_shared_runners_toggle: false)
end
post :toggle_shared_runners, params: params it 'toggles shared_runners_enabled when the group allows shared runners' do
project.update!(shared_runners_enabled: true)
project.reload post :toggle_shared_runners, params: params
expect(response).to have_gitlab_http_status(:found) project.reload
expect(project.shared_runners_enabled).to eq(false)
end
it 'toggles shared_runners_enabled when the group disallows shared runners but allows overrides' do expect(response).to have_gitlab_http_status(:found)
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: true) expect(project.shared_runners_enabled).to eq(false)
project.update!(shared_runners_enabled: false) end
post :toggle_shared_runners, params: params it 'toggles shared_runners_enabled when the group disallows shared runners but allows overrides' do
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: true)
project.update!(shared_runners_enabled: false)
project.reload post :toggle_shared_runners, params: params
expect(response).to have_gitlab_http_status(:found) project.reload
expect(project.shared_runners_enabled).to eq(true)
expect(response).to have_gitlab_http_status(:found)
expect(project.shared_runners_enabled).to eq(true)
end
it 'does not enable if the group disallows shared runners' do
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: false)
project.update!(shared_runners_enabled: false)
post :toggle_shared_runners, params: params
project.reload
expect(response).to have_gitlab_http_status(:found)
expect(project.shared_runners_enabled).to eq(false)
expect(flash[:alert]).to eq('Cannot enable shared runners because parent group does not allow it')
end
end end
it 'does not enable if the group disallows shared runners' do context 'with feature flag: vueify_shared_runners_toggle' do
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: false) it 'toggles shared_runners_enabled when the group allows shared runners' do
project.update!(shared_runners_enabled: false) project.update!(shared_runners_enabled: true)
post :toggle_shared_runners, params: params post :toggle_shared_runners, params: params
project.reload project.reload
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:ok)
expect(project.shared_runners_enabled).to eq(false) expect(project.shared_runners_enabled).to eq(false)
expect(flash[:alert]).to eq("Cannot enable shared runners because parent group does not allow it") end
it 'toggles shared_runners_enabled when the group disallows shared runners but allows overrides' do
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: true)
project.update!(shared_runners_enabled: false)
post :toggle_shared_runners, params: params
project.reload
expect(response).to have_gitlab_http_status(:ok)
expect(project.shared_runners_enabled).to eq(true)
end
it 'does not enable if the group disallows shared runners' do
group.update!(shared_runners_enabled: false, allow_descendants_override_disabled_shared_runners: false)
project.update!(shared_runners_enabled: false)
post :toggle_shared_runners, params: params
project.reload
expect(response).to have_gitlab_http_status(:unauthorized)
expect(project.shared_runners_enabled).to eq(false)
expect(json_response['error']).to eq('Cannot enable shared runners because parent group does not allow it')
end
end end
end end
end end
...@@ -179,16 +179,32 @@ RSpec.describe 'Runners' do ...@@ -179,16 +179,32 @@ RSpec.describe 'Runners' do
context 'when a project has disabled shared_runners' do context 'when a project has disabled shared_runners' do
let(:project) { create(:project, shared_runners_enabled: false) } let(:project) { create(:project, shared_runners_enabled: false) }
before do context 'when feature flag: vueify_shared_runners_toggle is disabled' do
project.add_maintainer(user) before do
stub_feature_flags(vueify_shared_runners_toggle: false)
project.add_maintainer(user)
end
it 'user enables shared runners' do
visit project_runners_path(project)
click_on 'Enable shared runners'
expect(page.find('.shared-runners-description')).to have_content('Disable shared runners')
expect(page).not_to have_selector('#toggle-shared-runners-form')
end
end end
it 'user enables shared runners' do context 'when feature flag: vueify_shared_runners_toggle is enabled' do
visit project_runners_path(project) before do
project.add_maintainer(user)
end
click_on 'Enable shared runners' it 'user enables shared runners' do
visit project_runners_path(project)
expect(page.find('.shared-runners-description')).to have_content('Disable shared runners') expect(page).to have_selector('#toggle-shared-runners-form')
end
end end
end end
......
import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAxiosAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import SharedRunnersToggleComponent from '~/projects/settings/components/shared_runners_toggle.vue';
import axios from '~/lib/utils/axios_utils';
const TEST_UPDATE_PATH = '/test/update_shared_runners';
jest.mock('~/flash');
describe('projects/settings/components/shared_runners', () => {
let wrapper;
let mockAxios;
const createComponent = (props = {}) => {
wrapper = shallowMount(SharedRunnersToggleComponent, {
propsData: {
isEnabled: false,
isDisabledAndUnoverridable: false,
isLoading: false,
updatePath: TEST_UPDATE_PATH,
...props,
},
});
};
const findErrorAlert = () => wrapper.find(GlAlert);
const findSharedRunnersToggle = () => wrapper.find(GlToggle);
const findToggleTooltip = () => wrapper.find(GlTooltip);
const getToggleValue = () => findSharedRunnersToggle().props('value');
const isToggleLoading = () => findSharedRunnersToggle().props('isLoading');
const isToggleDisabled = () => findSharedRunnersToggle().props('disabled');
beforeEach(() => {
mockAxios = new MockAxiosAdapter(axios);
mockAxios.onPost(TEST_UPDATE_PATH).reply(200);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mockAxios.restore();
});
describe('with group share settings DISABLED', () => {
beforeEach(() => {
createComponent({
isDisabledAndUnoverridable: true,
});
});
it('toggle should be disabled', () => {
expect(isToggleDisabled()).toBe(true);
});
it('tooltip should exist explaining why the toggle is disabled', () => {
expect(findToggleTooltip().exists()).toBe(true);
});
});
describe('with group share settings ENABLED', () => {
beforeEach(() => {
createComponent();
});
it('toggle should be enabled', () => {
expect(isToggleDisabled()).toBe(false);
});
it('loading icon, error message, and tooltip should not exist', () => {
expect(isToggleLoading()).toBe(false);
expect(findErrorAlert().exists()).toBe(false);
expect(findToggleTooltip().exists()).toBe(false);
});
describe('with shared runners DISABLED', () => {
beforeEach(() => {
createComponent();
});
it('toggle should be turned off', () => {
expect(getToggleValue()).toBe(false);
});
it('can enable toggle', async () => {
findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
expect(mockAxios.history.post[0].data).toEqual(undefined);
expect(mockAxios.history.post).toHaveLength(1);
expect(findErrorAlert().exists()).toBe(false);
expect(getToggleValue()).toBe(true);
});
});
describe('with shared runners ENABLED', () => {
beforeEach(() => {
createComponent({ isEnabled: true });
});
it('toggle should be turned on', () => {
expect(getToggleValue()).toBe(true);
});
it('can disable toggle', async () => {
findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
expect(mockAxios.history.post[0].data).toEqual(undefined);
expect(mockAxios.history.post).toHaveLength(1);
expect(findErrorAlert().exists()).toBe(false);
expect(getToggleValue()).toBe(false);
});
});
describe('loading icon', () => {
it('should show and hide on request', async () => {
createComponent();
expect(isToggleLoading()).toBe(false);
findSharedRunnersToggle().vm.$emit('change', true);
await wrapper.vm.$nextTick();
expect(isToggleLoading()).toBe(true);
await waitForPromises();
expect(isToggleLoading()).toBe(false);
});
});
describe('when request encounters an error', () => {
it('should show custom error message from API if it exists', async () => {
mockAxios.onPost(TEST_UPDATE_PATH).reply(401, { error: 'Custom API Error message' });
createComponent();
expect(getToggleValue()).toBe(false);
findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
expect(findErrorAlert().text()).toBe('Custom API Error message');
expect(getToggleValue()).toBe(false); // toggle value should not change
});
it('should show default error message if API does not return a custom error message', async () => {
mockAxios.onPost(TEST_UPDATE_PATH).reply(401);
createComponent();
expect(getToggleValue()).toBe(false);
findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
expect(findErrorAlert().text()).toBe('An error occurred while updating the configuration.');
expect(getToggleValue()).toBe(false); // toggle value should not change
});
});
});
});
...@@ -74,4 +74,57 @@ RSpec.describe Ci::RunnersHelper do ...@@ -74,4 +74,57 @@ RSpec.describe Ci::RunnersHelper do
expect(data[:parent_shared_runners_availability]).to eq('enabled') expect(data[:parent_shared_runners_availability]).to eq('enabled')
end end
end end
describe '#toggle_shared_runners_settings_data' do
let_it_be(:group) { create(:group) }
let(:project_with_runners) { create(:project, namespace: group, shared_runners_enabled: true) }
let(:project_without_runners) { create(:project, namespace: group, shared_runners_enabled: false) }
context 'when project has runners' do
it 'returns the correct value for is_enabled' do
data = toggle_shared_runners_settings_data(project_with_runners)
expect(data[:is_enabled]).to eq("true")
end
end
context 'when project does not have runners' do
it 'returns the correct value for is_enabled' do
data = toggle_shared_runners_settings_data(project_without_runners)
expect(data[:is_enabled]).to eq("false")
end
end
context 'for all projects' do
it 'returns the update path for toggling the shared runners setting' do
data = toggle_shared_runners_settings_data(project_with_runners)
expect(data[:update_path]).to eq(toggle_shared_runners_project_runners_path(project_with_runners))
end
it 'returns false for is_disabled_and_unoverridable when project has no group' do
project = create(:project)
data = toggle_shared_runners_settings_data(project)
expect(data[:is_disabled_and_unoverridable]).to eq("false")
end
using RSpec::Parameterized::TableSyntax
where(:shared_runners_setting, :is_disabled_and_unoverridable) do
'enabled' | "false"
'disabled_with_override' | "false"
'disabled_and_unoverridable' | "true"
end
with_them do
it 'returns the override runner status for project with group' do
group = create(:group)
project = create(:project, group: group)
allow(group).to receive(:shared_runners_setting).and_return(shared_runners_setting)
data = toggle_shared_runners_settings_data(project)
expect(data[:is_disabled_and_unoverridable]).to eq(is_disabled_and_unoverridable)
end
end
end
end
end end
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