Skip to content
Snippets Groups Projects
Commit 73723655 authored by Eric Eastwood's avatar Eric Eastwood
Browse files
parent 8e3f40f7
No related branches found
No related tags found
No related merge requests found
Showing
with 225 additions and 155 deletions
Loading
Loading
@@ -14,6 +14,7 @@ import {
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import applications from './components/applications.vue';
import setupToggleButtons from '../toggle_buttons';
 
/**
* Cluster page has 2 separate parts:
Loading
Loading
@@ -48,12 +49,9 @@ export default class Clusters {
installPrometheusEndpoint: installPrometheusPath,
});
 
this.toggle = this.toggle.bind(this);
this.installApplication = this.installApplication.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.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
Loading
Loading
@@ -63,6 +61,7 @@ export default class Clusters {
this.tokenField = document.querySelector('.js-cluster-token');
 
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications();
 
if (this.store.state.status !== 'created') {
Loading
Loading
@@ -101,13 +100,11 @@ export default class Clusters {
}
 
addListeners() {
this.toggleButton.addEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
}
 
removeListeners() {
this.toggleButton.removeEventListener('click', this.toggle);
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
eventHub.$off('installApplication', this.installApplication);
}
Loading
Loading
@@ -151,11 +148,6 @@ export default class Clusters {
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() {
const type = this.tokenField.getAttribute('type');
 
Loading
Loading
import Flash from '../flash';
import { s__ } from '../locale';
import setupToggleButtons from '../toggle_buttons';
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');
};
 
/**
* Toggles checked class for the given button
* @param {HTMLElement} button
*/
const toggleValue = (button) => {
button.classList.toggle('is-checked');
export default () => {
const clusterList = document.querySelector('.js-clusters-list');
// The empty state won't have a clusterList
if (clusterList) {
setupToggleButtons(
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));
});
}
Loading
Loading
@@ -12,11 +12,12 @@
.table-section.section-10
.table-mobile-header{ role: "rowheader" }
.table-mobile-content
%button{ type: "button",
class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_cluster?,
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")
%span.toggle-icon
= sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
Loading
Loading
Loading
Loading
@@ -10,13 +10,12 @@
= s_('ClusterIntegration|Cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
%label.append-bottom-10
= field.hidden_field :enabled, { class: 'js-toggle-input'}
%label.append-bottom-10.js-cluster-enable-toggle-area
%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"),
disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon
= 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')
Loading
Loading
Loading
Loading
@@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do
 
context 'when user disables the cluster' 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' }
end
 
Loading
Loading
Loading
Loading
@@ -62,7 +62,7 @@ feature 'User Cluster', :js do
 
context 'when user disables the cluster' 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'
page.within('#cluster-integration') { click_button 'Save changes' }
end
Loading
Loading
Loading
Loading
@@ -37,13 +37,13 @@ feature 'Clusters', :js do
 
context 'inline update of 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
 
context 'with sucessfull request' do
it 'user sees updated cluster' do
expect do
page.find('.js-toggle-cluster-list').click
page.find('.js-project-feature-toggle').click
wait_for_requests
end.to change { cluster.reload.enabled }
 
Loading
Loading
@@ -57,7 +57,7 @@ feature 'Clusters', :js do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
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_selector('.is-checked')
Loading
Loading
Loading
Loading
@@ -23,16 +23,24 @@ describe('Clusters', () => {
});
 
describe('toggle', () => {
it('should update the button and the input field on click', () => {
cluster.toggleButton.click();
it('should update the button and the input field on click', (done) => {
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(
cluster.toggleButton.classList,
).not.toContain('is-checked');
toggleButton.click();
 
expect(
cluster.toggleInput.getAttribute('value'),
).toEqual('false');
getSetTimeoutPromise()
.then(() => {
expect(
toggleButton.classList,
).not.toContain('is-checked');
expect(
toggleInput.getAttribute('value'),
).toEqual('false');
})
.then(done)
.catch(done.fail);
});
});
 
Loading
Loading
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);
});
});
});
Loading
Loading
@@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success
store_frontend_fixture(response, example.description)
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
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();
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment