Commit d3714bd5 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch 'toggle-shared-runners' into 'master'

Migrate button toggle for shared project runners to Vue

See merge request gitlab-org/gitlab!48452
parents 760e3f96 059bd0af
......@@ -4,6 +4,7 @@ import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
......@@ -32,4 +33,8 @@ document.addEventListener('DOMContentLoaded', () => {
initDeployFreeze();
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
def toggle_shared_runners
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
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
def toggle_group_runners
......
......@@ -11,6 +11,7 @@ module Projects
before_action :define_variables
before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
push_frontend_feature_flag(:vueify_shared_runners_toggle, @project)
end
helper_method :highlight_badge
......
......@@ -49,6 +49,14 @@ module Ci
parent_shared_runners_availability: group.parent&.shared_runners_setting
}
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
......
- isVueifySharedRunnersToggleEnabled = Feature.enabled?(:vueify_shared_runners_toggle, @project)
= render layout: 'shared/runners/shared_runners_description' do
%hr
- if @project.group&.shared_runners_setting == 'disabled_and_unoverridable'
%h5.gl-text-red-500
= _('Shared runners disabled on group level')
- else
- if @project.shared_runners_enabled?
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
= _('Disable shared runners')
- if !isVueifySharedRunnersToggleEnabled
%hr
- if @project.group&.shared_runners_setting == 'disabled_and_unoverridable'
%h5.gl-text-red-500
= _('Shared runners disabled on group level')
- 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 @project.shared_runners_enabled?
= link_to toggle_shared_runners_project_runners_path(@project), class: 'btn btn-close', method: :post do
= _('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
= _('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 ""
msgid "An error occurred while updating the comment"
msgstr ""
msgid "An error occurred while updating the configuration."
msgstr ""
msgid "An error occurred while updating the milestone."
msgstr ""
......@@ -10449,6 +10452,9 @@ msgstr ""
msgid "Enable shared runners for this group"
msgstr ""
msgid "Enable shared runners for this project"
msgstr ""
msgid "Enable smartcn custom analyzer: Indexing"
msgstr ""
......@@ -25255,6 +25261,9 @@ msgstr ""
msgid "Shared runners are disabled for the parent group"
msgstr ""
msgid "Shared runners are disabled on group level"
msgstr ""
msgid "Shared runners disabled on group level"
msgstr ""
......
......@@ -78,40 +78,84 @@ RSpec.describe Projects::RunnersController do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
it 'toggles shared_runners_enabled when the group allows shared runners' do
project.update!(shared_runners_enabled: true)
context 'without feature flag' do
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)
expect(project.shared_runners_enabled).to eq(false)
end
project.reload
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)
expect(response).to have_gitlab_http_status(:found)
expect(project.shared_runners_enabled).to eq(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)
expect(project.shared_runners_enabled).to eq(true)
project.reload
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
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)
context 'with feature flag: vueify_shared_runners_toggle' do
it 'toggles shared_runners_enabled when the group allows shared runners' do
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(project.shared_runners_enabled).to eq(false)
expect(flash[:alert]).to eq("Cannot enable shared runners because parent group does not allow it")
expect(response).to have_gitlab_http_status(:ok)
expect(project.shared_runners_enabled).to eq(false)
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
......@@ -179,16 +179,32 @@ RSpec.describe 'Runners' do
context 'when a project has disabled shared_runners' do
let(:project) { create(:project, shared_runners_enabled: false) }
before do
project.add_maintainer(user)
context 'when feature flag: vueify_shared_runners_toggle is disabled' do
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
it 'user enables shared runners' do
visit project_runners_path(project)
context 'when feature flag: vueify_shared_runners_toggle is enabled' do
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
......
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
expect(data[:parent_shared_runners_availability]).to eq('enabled')
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
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