diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js new file mode 100644 index 00000000000..52dfed5668a --- /dev/null +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -0,0 +1,62 @@ +import Flash from '../flash'; +import { s__ } from '../locale'; +import ClustersService from './services/clusters_service'; + +/** + * Handles toggle buttons in the cluster's table. + * + * When the user clicks the toggle button for each cluster, it: + * - toggles the button + * - shows a loding and disabled state + * - 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 class ClusterTable { + constructor() { + this.container = '.js-clusters-list'; + document.querySelectorAll(`${this.container} .js-toggle-cluster-list`).forEach(button => button.addEventListener('click', e => ClusterTable.updateCluster(e))); + } + + removeListeners() { + document.querySelectorAll(`${this.container} .js-toggle-cluster-list`).forEach(button => button.removeEventListener('click')); + } + + static updateCluster(e) { + const toggleButton = e.currentTarget; + const value = toggleButton.classList.contains('checked').toString(); + const endpoint = toggleButton.getAttribute('data-endpoint'); + + ClusterTable.toggleValue(toggleButton); + ClusterTable.toggleLoadingButton(toggleButton); + + ClustersService.updateCluster(endpoint, { cluster: { enabled: value } }) + .then(() => { + ClusterTable.toggleLoadingButton(toggleButton); + }) + .catch(() => { + ClusterTable.toggleLoadingButton(toggleButton); + ClusterTable.toggleValue(toggleButton); + Flash(s__('ClusterIntegration|Something went wrong on our end.')); + }); + } + + /** + * Toggles loading and disabled classes. + * @param {HTMLElement} button + */ + static toggleLoadingButton(button) { + button.setAttribute('disabled', button.getAttribute('disabled')); + button.classList.toggle('disabled'); + button.classList.toggle('loading'); + } + + /** + * Toggles checked class for the given button + * @param {HTMLElement} button + */ + static toggleValue(button) { + button.classList.toggle('checked'); + } +} diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js index ce14c9a9945..755c2981c2e 100644 --- a/app/assets/javascripts/clusters/services/clusters_service.js +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -17,4 +17,8 @@ export default class ClusterService { installApplication(appId) { return axios.post(this.appInstallEndpointMap[appId]); } + + static updateCluster(endpoint, data) { + return axios.put(endpoint, data); + } } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 34708977d20..49459f01bea 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -550,7 +550,15 @@ import ProjectVariables from './project_variables'; import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') .then(cluster => new cluster.default()) // eslint-disable-line new-cap .catch((err) => { - Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript')); + Flash(s__('ClusterIntegration|Problem setting up the cluster')); + throw err; + }); + break; + case 'projects:clusters:index': + import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index') + .then(clusterIndex => new clusterIndex.default()) // eslint-disable-line new-cap + .catch((err) => { + Flash(s__('ClusterIntegration|Problem setting up the clusters list')); throw err; }); break; diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 19ae3192044..37271c4708d 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -7,7 +7,7 @@ class Projects::ClustersController < Projects::ApplicationController before_action :authorize_admin_cluster!, only: [:destroy] def index - @clusters ||= project.clusters.map { |cluster| cluster.present(current_user: current_user) } + @clusters ||= project.clusters.page(params[:page]).per(20).map { |cluster| cluster.present(current_user: current_user) } end def login @@ -64,10 +64,20 @@ class Projects::ClustersController < Projects::ApplicationController .execute(cluster) if cluster.valid? - flash[:notice] = "Cluster was successfully updated." - redirect_to project_cluster_path(project, project.cluster) + respond_to do |format| + format.json do + head :no_content + end + format.html do + flash[:notice] = "Cluster was successfully updated." + redirect_to project_cluster_path(project, project.cluster) + end + end else - render :show + respond_to do |format| + format.json { head :bad_request } + format.html { render :show } + end end end diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index 44f13a009b9..4554f5c624d 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -5,6 +5,6 @@ %p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} %p - = link_to s_('ClusterIntegration|Add cluster'), '', class: 'btn btn-success', title: 'Add cluster' + = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success', title: 'Add cluster' .svg-content = image_tag 'illustrations/labels.svg' diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml index 6b7fbb44c4a..d7e0940cb65 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/projects/clusters/index.html.haml @@ -21,8 +21,8 @@ %span.badge 0 .nav-controls - = link_to s_('ClusterIntegration|Add cluster'), '', class: 'btn btn-success', title: 'Add cluster' - .ci-table + = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success', title: 'Add cluster' + .ci-table.js-clusters-list .gl-responsive-table-row.table-row-header{ role: 'row' } .table-section.section-30{ role: 'rowheader' } = s_('ClusterIntegration|Cluster') @@ -31,27 +31,30 @@ .table-section.section-30{ role: 'rowheader' } = s_('ClusterIntegration|Project namespace') .table-section.section-10{ role: 'rowheader' } - .gl-responsive-table-row - .table-section.section-30 - .table-mobile-header{ role: 'rowheader' } - = s_('ClusterIntegration|Cluster') - .table-mobile-content - Content goes here - .table-section.section-30 - .table-mobile-header{ role: 'rowheader' } - = s_('ClusterIntegration|Environment pattern') - .table-mobile-content - Content goes here - .table-section.section-30 - .table-mobile-header{ role: 'rowheader' } - = s_('ClusterIntegration|Project namespace') - .table-mobile-content - Content goes here - .table-section.section-10 - .table-mobile-header{ role: 'rowheader' } - .table-mobile-content - %button{ type: 'button', - class: "js-toggle-cluster project-feature-toggle", - 'aria-label': s_('ClusterIntegration|Toggle Cluster'), - data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } } + - @clusters.each do |cluster| + .gl-responsive-table-row + .table-section.section-30 + .table-mobile-header{ role: 'rowheader' }= s_('ClusterIntegration|Cluster') + .table-mobile-content= cluster.name + .table-section.section-30 + .table-mobile-header{ role: 'rowheader' } + = s_('ClusterIntegration|Environment pattern') + .table-mobile-content + Content goes here + .table-section.section-30 + .table-mobile-header{ role: 'rowheader' } + = s_('ClusterIntegration|Project namespace') + .table-mobile-content + Content goes here + .table-section.section-10 + .table-mobile-header{ role: 'rowheader' } + .table-mobile-content + %button{ type: 'button', + class: "js-toggle-cluster-list project-feature-toggle #{'checked' unless !cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, cluster)}", + 'aria-label': s_('ClusterIntegration|Toggle Cluster'), + disabled: !can?(current_user, :update_cluster, cluster), + data: { 'enabled-text': 'Enabled', + 'disabled-text': 'Disabled', + endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } + = icon('loading', class: 'hidden') diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index 21f00b4e266..dc78ac48cc9 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -94,6 +94,33 @@ feature 'Clusters', :js do visit project_clusters_path(project) end + it 'user sees a table with one cluster' do + + end + + it 'user sees a disabled add cluster button ' do + + end + + it 'user sees navigation tabs' do + + end + + context 'update cluster' do + it 'user can update cluster' do + end + + context 'with sucessfull request' do + it 'user sees updated cluster' do + end + end + + context 'with failed request' do + it 'user sees not update cluster and error message' do + end + end + end + context 'when user clicks on a cluster' do before do # TODO: Replace with Click on cluster after frontend implements list @@ -216,4 +243,6 @@ feature 'Clusters', :js do expect(page).to have_css('.signin-with-google') end end + + context end diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js new file mode 100644 index 00000000000..8798f5c37f0 --- /dev/null +++ b/spec/javascripts/clusters/clusters_index_spec.js @@ -0,0 +1,31 @@ +import ClusterTable from '~/clusters/clusters_index'; + +describe('Clusters table', () => { + let ClustersClass; + + beforeEach(() => { + ClustersClass = new ClusterTable(); + }); + + afterEach(() => { + ClustersClass.removeListeners(); + }); + + describe('update cluster', () => { + it('renders a toggle button', () => { + + }); + + it('renders loading state while request is made', () => { + + }); + + it('shows updated state after sucessfull request', () => { + + }); + + it('shows inital state after failed request', () => { + + }); + }); +});