Commit 44c2f802 authored by Matija Čupić's avatar Matija Čupić

Merge branch 'master' into fix-cache-clear-windows

parents 87c4e874 dc325c67
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue'; import applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
/** /**
* Cluster page has 2 separate parts: * Cluster page has 2 separate parts:
...@@ -48,12 +49,9 @@ export default class Clusters { ...@@ -48,12 +49,9 @@ export default class Clusters {
installPrometheusEndpoint: installPrometheusPath, installPrometheusEndpoint: installPrometheusPath,
}); });
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
this.showToken = this.showToken.bind(this); this.showToken = this.showToken.bind(this);
this.toggleButton = document.querySelector('.js-toggle-cluster');
this.toggleInput = document.querySelector('.js-toggle-input');
this.errorContainer = document.querySelector('.js-cluster-error'); this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success'); this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating'); this.creatingContainer = document.querySelector('.js-cluster-creating');
...@@ -63,6 +61,7 @@ export default class Clusters { ...@@ -63,6 +61,7 @@ export default class Clusters {
this.tokenField = document.querySelector('.js-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token');
initSettingsPanels(); initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(); this.initApplications();
if (this.store.state.status !== 'created') { if (this.store.state.status !== 'created') {
...@@ -101,13 +100,11 @@ export default class Clusters { ...@@ -101,13 +100,11 @@ export default class Clusters {
} }
addListeners() { addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication); eventHub.$on('installApplication', this.installApplication);
} }
removeListeners() { removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken); if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication); eventHub.$off('installApplication', this.installApplication);
} }
...@@ -151,11 +148,6 @@ export default class Clusters { ...@@ -151,11 +148,6 @@ export default class Clusters {
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
} }
toggle() {
this.toggleButton.classList.toggle('is-checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
}
showToken() { showToken() {
const type = this.tokenField.getAttribute('type'); const type = this.tokenField.getAttribute('type');
......
import Flash from '../flash'; import Flash from '../flash';
import { s__ } from '../locale'; import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
/**
* Toggles loading and disabled classes.
* @param {HTMLElement} button
*/
const toggleLoadingButton = (button) => {
if (button.getAttribute('disabled')) {
button.removeAttribute('disabled');
} else {
button.setAttribute('disabled', true);
}
button.classList.toggle('is-loading');
};
/** export default () => {
* Toggles checked class for the given button const clusterList = document.querySelector('.js-clusters-list');
* @param {HTMLElement} button // The empty state won't have a clusterList
*/ if (clusterList) {
const toggleValue = (button) => { setupToggleButtons(
button.classList.toggle('is-checked'); document.querySelector('.js-clusters-list'),
(value, toggle) =>
ClustersService.updateCluster(toggle.dataset.endpoint, { cluster: { enabled: value } })
.catch((err) => {
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
throw err;
}),
);
}
}; };
/**
* Handles toggle buttons in the cluster's table.
*
* When the user clicks the toggle button for each cluster, it:
* - toggles the button
* - shows a loading and disables button
* - Makes a put request to the given endpoint
* Once we receive the response, either:
* 1) Show updated status in case of successfull response
* 2) Show initial status in case of failed response
*/
export default function setClusterTableToggles() {
document.querySelectorAll('.js-toggle-cluster-list')
.forEach(button => button.addEventListener('click', (e) => {
const toggleButton = e.currentTarget;
const endpoint = toggleButton.getAttribute('data-endpoint');
toggleValue(toggleButton);
toggleLoadingButton(toggleButton);
const value = toggleButton.classList.contains('is-checked');
ClustersService.updateCluster(endpoint, { cluster: { enabled: value } })
.then(() => {
toggleLoadingButton(toggleButton);
})
.catch(() => {
toggleLoadingButton(toggleButton);
toggleValue(toggleButton);
Flash(s__('ClusterIntegration|Something went wrong on our end.'));
});
}));
}
import $ from 'jquery';
import Flash from './flash';
import { __ } from './locale';
import { convertPermissionToBoolean } from './lib/utils/common_utils';
/*
example HAML:
```
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if enabled?}",
'aria-label': _('Toggle Cluster') }
%input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? }
```
*/
function updatetoggle(toggle, isOn) {
toggle.classList.toggle('is-checked', isOn);
}
function onToggleClicked(toggle, input, clickCallback) {
const previousIsOn = convertPermissionToBoolean(input.value);
// Visually change the toggle and start loading
updatetoggle(toggle, !previousIsOn);
toggle.setAttribute('disabled', true);
toggle.classList.toggle('is-loading', true);
Promise.resolve(clickCallback(!previousIsOn, toggle))
.then(() => {
// Actually change the input value
input.setAttribute('value', !previousIsOn);
})
.catch(() => {
// Revert the visuals if something goes wrong
updatetoggle(toggle, previousIsOn);
})
.then(() => {
// Remove the loading indicator in any case
toggle.removeAttribute('disabled');
toggle.classList.toggle('is-loading', false);
$(input).trigger('trigger-change');
})
.catch(() => {
Flash(__('Something went wrong when toggling the button'));
});
}
export default function setupToggleButtons(container, clickCallback = () => {}) {
const toggles = container.querySelectorAll('.js-project-feature-toggle');
toggles.forEach((toggle) => {
const input = toggle.querySelector('.js-project-feature-toggle-input');
const isOn = convertPermissionToBoolean(input.value);
// Get the visible toggle in sync with the hidden input
updatetoggle(toggle, isOn);
toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback));
});
}
...@@ -5,15 +5,16 @@ ...@@ -5,15 +5,16 @@
= markdown_field(current_application_settings, :help_page_text) = markdown_field(current_application_settings, :help_page_text)
%hr %hr
- unless current_application_settings.help_page_hide_commercial_content? %h1
%h1 GitLab
GitLab Community Edition
Community Edition - if user_signed_in?
- if user_signed_in? %span= Gitlab::VERSION
%span= Gitlab::VERSION %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) = version_status_badge
= version_status_badge %hr
- unless current_application_settings.help_page_hide_commercial_content?
%p.slead %p.slead
GitLab is open source software to collaborate on code. GitLab is open source software to collaborate on code.
%br %br
......
...@@ -12,11 +12,12 @@ ...@@ -12,11 +12,12 @@
.table-section.section-10 .table-section.section-10
.table-mobile-header{ role: "rowheader" } .table-mobile-header{ role: "rowheader" }
.table-mobile-content .table-mobile-content
%button{ type: "button", %button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?, disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
= icon("spinner spin", class: "loading-icon") = icon("spinner spin", class: "loading-icon")
%span.toggle-icon %span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
......
...@@ -10,13 +10,12 @@ ...@@ -10,13 +10,12 @@
= s_('ClusterIntegration|Cluster integration is enabled for this project.') = s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else - else
= s_('ClusterIntegration|Cluster integration is disabled for this project.') = s_('ClusterIntegration|Cluster integration is disabled for this project.')
%label.append-bottom-10 %label.append-bottom-10.js-cluster-enable-toggle-area
= field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button', %button{ type: 'button',
class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster) } disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon %span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
= sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
......
---
title: Fix version information not showing on help page if commercial content display
was disabled.
merge_request: 16743
author:
type: fixed
...@@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do ...@@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do
context 'when user disables the cluster' do context 'when user disables the cluster' do
before do before do
page.find(:css, '.js-toggle-cluster').click page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
page.within('#cluster-integration') { click_button 'Save changes' } page.within('#cluster-integration') { click_button 'Save changes' }
end end
......
...@@ -62,7 +62,7 @@ feature 'User Cluster', :js do ...@@ -62,7 +62,7 @@ feature 'User Cluster', :js do
context 'when user disables the cluster' do context 'when user disables the cluster' do
before do before do
page.find(:css, '.js-toggle-cluster').click page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
fill_in 'cluster_name', with: 'dev-cluster' fill_in 'cluster_name', with: 'dev-cluster'
page.within('#cluster-integration') { click_button 'Save changes' } page.within('#cluster-integration') { click_button 'Save changes' }
end end
......
...@@ -37,13 +37,13 @@ feature 'Clusters', :js do ...@@ -37,13 +37,13 @@ feature 'Clusters', :js do
context 'inline update of cluster' do context 'inline update of cluster' do
it 'user can update cluster' do it 'user can update cluster' do
expect(page).to have_selector('.js-toggle-cluster-list') expect(page).to have_selector('.js-project-feature-toggle')
end end
context 'with sucessfull request' do context 'with sucessfull request' do
it 'user sees updated cluster' do it 'user sees updated cluster' do
expect do expect do
page.find('.js-toggle-cluster-list').click page.find('.js-project-feature-toggle').click
wait_for_requests wait_for_requests
end.to change { cluster.reload.enabled } end.to change { cluster.reload.enabled }
...@@ -57,7 +57,7 @@ feature 'Clusters', :js do ...@@ -57,7 +57,7 @@ feature 'Clusters', :js do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false } allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
page.find('.js-toggle-cluster-list').click page.find('.js-project-feature-toggle').click
expect(page).to have_content('Something went wrong on our end.') expect(page).to have_content('Something went wrong on our end.')
expect(page).to have_selector('.is-checked') expect(page).to have_selector('.is-checked')
......
...@@ -23,16 +23,24 @@ describe('Clusters', () => { ...@@ -23,16 +23,24 @@ describe('Clusters', () => {
}); });
describe('toggle', () => { describe('toggle', () => {
it('should update the button and the input field on click', () => { it('should update the button and the input field on click', (done) => {
cluster.toggleButton.click(); const toggleButton = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle');
const toggleInput = document.querySelector('.js-cluster-enable-toggle-area .js-project-feature-toggle-input');
expect( toggleButton.click();
cluster.toggleButton.classList,
).not.toContain('is-checked');
expect( getSetTimeoutPromise()
cluster.toggleInput.getAttribute('value'), .then(() => {
).toEqual('false'); expect(
toggleButton.classList,
).not.toContain('is-checked');
expect(
toggleInput.getAttribute('value'),
).toEqual('false');
})
.then(done)
.catch(done.fail);
}); });
}); });
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import setClusterTableToggles from '~/clusters/clusters_index';
import { setTimeout } from 'core-js/library/web/timers';
describe('Clusters table', () => {
preloadFixtures('clusters/index_cluster.html.raw');
let mock;
beforeEach(() => {
loadFixtures('clusters/index_cluster.html.raw');
mock = new MockAdapter(axios);
setClusterTableToggles();
});
describe('update cluster', () => {
it('renders loading state while request is made', () => {
const button = document.querySelector('.js-toggle-cluster-list');
button.click();
expect(button.classList).toContain('is-loading');
expect(button.getAttribute('disabled')).toEqual('true');
});
afterEach(() => {
mock.restore();
});
it('shows updated state after sucessfull request', (done) => {
mock.onPut().reply(200, {}, {});
const button = document.querySelector('.js-toggle-cluster-list');
button.click();
expect(button.classList).toContain('is-loading');
setTimeout(() => {
expect(button.classList).not.toContain('is-loading');
expect(button.classList).not.toContain('is-checked');
done();
}, 0);
});
it('shows inital state after failed request', (done) => {
mock.onPut().reply(500, {}, {});
const button = document.querySelector('.js-toggle-cluster-list');
button.click();
expect(button.classList).toContain('is-loading');
setTimeout(() => {
expect(button.classList).not.toContain('is-loading');
expect(button.classList).toContain('is-checked');
done();
}, 0);
});
});
});
...@@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle ...@@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success expect(response).to be_success
store_frontend_fixture(response, example.description) store_frontend_fixture(response, example.description)
end end
context 'rendering non-empty state' do
before do
cluster
end
it 'clusters/index_cluster.html.raw' do |example|
get :index,
namespace_id: namespace,
project_id: project
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
end end
import setupToggleButtons from '~/toggle_buttons';
import getSetTimeoutPromise from './helpers/set_timeout_promise_helper';
function generateMarkup(isChecked = true) {
return `
<button type="button" class="${isChecked ? 'is-checked' : ''} js-project-feature-toggle">
<input type="hidden" class="js-project-feature-toggle-input" value="${isChecked}" />
</button>
`;
}
function setupFixture(isChecked, clickCallback) {
const wrapper = document.createElement('div');
wrapper.innerHTML = generateMarkup(isChecked);
setupToggleButtons(wrapper, clickCallback);
return wrapper;
}
describe('ToggleButtons', () => {
describe('when input value is true', () => {
it('should initialize as checked', () => {
const wrapper = setupFixture(true);
expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(true);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true');
});
it('should toggle to unchecked when clicked', (done) => {
const wrapper = setupFixture(true);
const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
toggleButton.click();
getSetTimeoutPromise()
.then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(false);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false');
})
.then(done)
.catch(done.fail);
});
});
describe('when input value is false', () => {
it('should initialize as unchecked', () => {
const wrapper = setupFixture(false);
expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(false);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false');
});
it('should toggle to checked when clicked', (done) => {
const wrapper = setupFixture(false);
const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
toggleButton.click();
getSetTimeoutPromise()
.then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(true);
expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true');
})
.then(done)
.catch(done.fail);
});
});
it('should emit `trigger-change` event', (done) => {
const changeSpy = jasmine.createSpy('changeEventHandler');
const wrapper = setupFixture(false);
const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
const input = wrapper.querySelector('.js-project-feature-toggle-input');
$(input).on('trigger-change', changeSpy);
toggleButton.click();
getSetTimeoutPromise()
.then(() => {
expect(changeSpy).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
describe('clickCallback', () => {
it('should show loading indicator while waiting', (done) => {
const isChecked = true;
const clickCallback = (newValue, toggleButton) => {
const input = toggleButton.querySelector('.js-project-feature-toggle-input');
expect(newValue).toEqual(false);
// Check for the loading state
expect(toggleButton.classList.contains('is-checked')).toEqual(false);
expect(toggleButton.classList.contains('is-loading')).toEqual(true);
expect(toggleButton.disabled).toEqual(true);
expect(input.value).toEqual('true');
// After the callback finishes, check that the loading state is gone
getSetTimeoutPromise()
.then(() => {
expect(toggleButton.classList.contains('is-checked')).toEqual(false);
expect(toggleButton.classList.contains('is-loading')).toEqual(false);
expect(toggleButton.disabled).toEqual(false);
expect(input.value).toEqual('false');
})
.then(done)
.catch(done.fail);
};
const wrapper = setupFixture(isChecked, clickCallback);
const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
toggleButton.click();
});
});
});
...@@ -1441,7 +1441,7 @@ describe API::Issues, :mailer do ...@@ -1441,7 +1441,7 @@ describe API::Issues, :mailer do
context 'when source project does not exist' do context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do it 'returns 404 when trying to move an issue' do
post api("/projects/123/issues/#{issue.iid}/move", user), post api("/projects/12345/issues/#{issue.iid}/move", user),
to_project_id: target_project.id to_project_id: target_project.id
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
...@@ -1452,7 +1452,7 @@ describe API::Issues, :mailer do ...@@ -1452,7 +1452,7 @@ describe API::Issues, :mailer do
context 'when target project does not exist' do context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user), post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
to_project_id: 123 to_project_id: 12345
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
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