diff options
-rw-r--r-- | app/assets/javascripts/clusters/clusters_index.js | 10 | ||||
-rw-r--r-- | app/assets/stylesheets/framework.scss | 1 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/blocks.scss | 9 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/common.scss | 8 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/toggle.scss | 139 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/projects.scss | 87 | ||||
-rw-r--r-- | app/controllers/projects/clusters_controller.rb | 2 | ||||
-rw-r--r-- | app/views/projects/clusters/_empty_state.html.haml | 20 | ||||
-rw-r--r-- | app/views/projects/clusters/index.html.haml | 36 | ||||
-rw-r--r-- | spec/features/projects/clusters_spec.rb | 13 | ||||
-rw-r--r-- | spec/javascripts/clusters/clusters_index_spec.js | 38 | ||||
-rw-r--r-- | spec/javascripts/fixtures/clusters.rb | 9 |
12 files changed, 243 insertions, 129 deletions
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 52dfed5668a..efdf2de5583 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -47,9 +47,15 @@ export default class ClusterTable { * @param {HTMLElement} button */ static toggleLoadingButton(button) { - button.setAttribute('disabled', button.getAttribute('disabled')); + if (button.getAttribute('disabled')) { + button.removeAttribute('disabled'); + } else { + button.setAttribute('disabled', true); + } + button.classList.toggle('disabled'); - button.classList.toggle('loading'); + button.classList.toggle('is-loading'); + button.querySelector('.loading-icon').classList.toggle('hidden'); } /** diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 66212be1b8f..43b16d3cf7d 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -44,6 +44,7 @@ @import "framework/tabs"; @import "framework/timeline"; @import "framework/tooltips"; +@import "framework/toggle"; @import "framework/typography"; @import "framework/zen"; @import "framework/blank"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 17096e25207..91976ca1f56 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -348,3 +348,12 @@ } } } + +.flex-container-block { + display: -webkit-flex; + display: flex; +} + +.flex-right { + margin-left: auto; +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 4316b1e87ae..5e4ddf366ef 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -454,11 +454,3 @@ img.emoji { .inline { display: inline-block; } .center { text-align: center; } .vertical-align-middle { vertical-align: middle; } - -.flex-justify-content-center { justify-content: center; } -.flex-wrap { flex-wrap: wrap; } -.flex-right { margin-left: auto; } -.flex-container-block { - display: -webkit-flex; - display: flex; -} diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss new file mode 100644 index 00000000000..e5ab604dab6 --- /dev/null +++ b/app/assets/stylesheets/framework/toggle.scss @@ -0,0 +1,139 @@ +/** +* Toggle button +* +* @usage +* ### Active text +* <button type="button" class="project-feature-toggle checked" data-enabled-text="Enabled" data-disabled-text="Disabled"> +* <i class="fa fa-spinner fa-spin loading-icon hidden"></i> +* </button> + +* ### Disabled text +* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled"> +* <i class="fa fa-spinner fa-spin loading-icon hidden"></i> +* </button> + +* ### Disabled button +* <button type="button" class="project-feature-toggle disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true"> +* <i class="fa fa-spinner fa-spin loading-icon hidden"></i> +* </button> + +* ### Loading +* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled"> +* <i class="fa fa-spinner fa-spin loading-icon"></i> +* </button> +*/ +.project-feature-toggle { + position: relative; + border: 0; + outline: 0; + display: block; + width: 100px; + height: 24px; + cursor: pointer; + user-select: none; + background: $feature-toggle-color-disabled; + border-radius: 12px; + padding: 3px; + transition: all .4s ease; + + &::selection, + &::before::selection, + &::after::selection { + background: none; + } + + &::before { + color: $feature-toggle-text-color; + font-size: 12px; + line-height: 24px; + position: absolute; + top: 0; + left: 25px; + right: 5px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + animation: animate-disabled .2s ease-in; + content: attr(data-disabled-text); + } + + &::after { + position: relative; + display: block; + content: ""; + width: 22px; + height: 18px; + left: 0; + border-radius: 9px; + background: $feature-toggle-color; + transition: all .2s ease; + } + + &.is-loading { + &::before { + left: 38px; + right: 5px; + } + + .loading-icon { + position: absolute; + left: 28px; + font-size: $tooltip-font-size; + color: $white-light; + top: 6px; + } + } + + &.checked { + background: $feature-toggle-color-enabled; + + &.is-loading { + &::before { + left: 10px; + right: 42px; + animation: animate-enabled .2s ease-in; + content: attr(data-enabled-text); + } + + .loading-icon { + left: 60px; + top: 6px; + } + } + + &::before { + left: 5px; + right: 25px; + animation: animate-enabled .2s ease-in; + content: attr(data-enabled-text); + } + + &::after { + left: calc(100% - 22px); + } + } + + &.disabled { + opacity: 0.4; + cursor: not-allowed; + } + + @media (max-width: $screen-xs-min) { + width: 50px; + + &::before, + &.checked::before { + display: none; + } + } + + @keyframes animate-enabled { + 0%, 35% { opacity: 0; } + 100% { opacity: 1; } + } + + @keyframes animate-disabled { + 0%, 35% { opacity: 0; } + 100% { opacity: 1; } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index aaad6dbba8e..baf3726c827 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -126,93 +126,6 @@ } } -.project-feature-toggle { - position: relative; - border: 0; - outline: 0; - display: block; - width: 100px; - height: 24px; - cursor: pointer; - user-select: none; - background: $feature-toggle-color-disabled; - border-radius: 12px; - padding: 3px; - transition: all .4s ease; - - &::selection, - &::before::selection, - &::after::selection { - background: none; - } - - &::before { - color: $feature-toggle-text-color; - font-size: 12px; - line-height: 24px; - position: absolute; - top: 0; - left: 25px; - right: 5px; - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - animation: animate-disabled .2s ease-in; - content: attr(data-disabled-text); - } - - &::after { - position: relative; - display: block; - content: ""; - width: 22px; - height: 18px; - left: 0; - border-radius: 9px; - background: $feature-toggle-color; - transition: all .2s ease; - } - - &.checked { - background: $feature-toggle-color-enabled; - - &::before { - left: 5px; - right: 25px; - animation: animate-enabled .2s ease-in; - content: attr(data-enabled-text); - } - - &::after { - left: calc(100% - 22px); - } - } - - &.disabled { - opacity: 0.4; - cursor: not-allowed; - } - - @media (max-width: $screen-xs-min) { - width: 50px; - - &::before, - &.checked::before { - display: none; - } - } - - @keyframes animate-enabled { - 0%, 35% { opacity: 0; } - 100% { opacity: 1; } - } - - @keyframes animate-disabled { - 0%, 35% { opacity: 0; } - 100% { opacity: 1; } - } -} - .project-home-panel, .group-home-panel { padding-top: 24px; diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 37271c4708d..19dbffed5bb 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -8,6 +8,8 @@ class Projects::ClustersController < Projects::ApplicationController def index @clusters ||= project.clusters.page(params[:page]).per(20).map { |cluster| cluster.present(current_user: current_user) } + + @clusters_count = @clusters.count end def login diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index 4554f5c624d..07e26cd9021 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -1,10 +1,12 @@ -.empty-state.flex-justify-content-center.flex-container-block.flex-wrap - %div - %h2= s_('ClusterIntegration|Integrate cluster automation') - - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - %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} +.row.empty-state + .col-xs-12 + .svg-content= image_tag 'illustrations/labels.svg' + .col-xs-12.text-center + .text-content + %h4= s_('ClusterIntegration|Integrate cluster automation') + - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') + %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'), new_project_cluster_path(@project), class: 'btn btn-success', title: 'Add cluster' - %p - = 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 d7e0940cb65..be6efbaa38b 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/projects/clusters/index.html.haml @@ -2,26 +2,26 @@ = render "empty_state" - else .top-area.scrolling-tabs-container.inner-page-scroll-tabs - .fade-left= icon('angle-left') - .fade-right= icon('angle-right') + .fade-left= icon("angle-left") + .fade-right= icon("angle-right") %ul.nav-links.scrolling-tabs %li - %a - = s_('ClusterIntegration|Active') + %a.js-active-tab + =s_("ClusterIntegration|Active") %span.badge - 0 + TODO + %li - %a - = s_('ClusterIntegration|Inactive') + %a.js-inactive-tab + = s_("ClusterIntegration|Inactive") %span.badge - 0 + TODO %li - %a - = s_('ClusterIntegration|All') - %span.badge - 0 + %a.js-all-tab + = s_("ClusterIntegration|All") + %span.badge= @clusters_count .nav-controls - = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success', title: 'Add cluster' + = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: "btn btn-success disabled has-tooltip js-add-cluster", title: s_("ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate") .ci-table.js-clusters-list .gl-responsive-table-row.table-row-header{ role: 'row' } .table-section.section-30{ role: 'rowheader' } @@ -35,17 +35,17 @@ .gl-responsive-table-row .table-section.section-30 .table-mobile-header{ role: 'rowheader' }= s_('ClusterIntegration|Cluster') - .table-mobile-content= cluster.name + .table-mobile-content + = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) .table-section.section-30 .table-mobile-header{ role: 'rowheader' } = s_('ClusterIntegration|Environment pattern') - .table-mobile-content - Content goes here + .table-mobile-content= cluster.environment_scope .table-section.section-30 .table-mobile-header{ role: 'rowheader' } = s_('ClusterIntegration|Project namespace') .table-mobile-content - Content goes here + Content goes here - TODO .table-section.section-10 .table-mobile-header{ role: 'rowheader' } .table-mobile-content @@ -56,5 +56,5 @@ data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled', endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } - = icon('loading', class: 'hidden') + = icon('spinner spin', class: 'hidden loading-icon') diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index dc78ac48cc9..3df4ef9d8c0 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -95,23 +95,32 @@ feature 'Clusters', :js do end it 'user sees a table with one cluster' do - + expect(page).to have_selector('.gl-responsive-table-row', count: 2) end it 'user sees a disabled add cluster button ' do - + expect(page.find(:css, '.js-add-cluster')['disabled']).to eq('true') end it 'user sees navigation tabs' do + expect(page.find('.js-active-tab').text).to include('Active') + expect(page.find('.js-active-tab .badge').text).to include('1') + expect(page.find('.js-inactive-tab').text).to include('Inactive') + expect(page.find('.js-inactive-tab .badge').text).to include('0') + + expect(page.find('.js-all-tab').text).to include('All') + expect(page.find('.js-all-tab .badge').text).to include('1') end context 'update cluster' do it 'user can update cluster' do + expect(page).to have_selector('.js-toggle-cluster-list') end context 'with sucessfull request' do it 'user sees updated cluster' do + end end diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js index 8798f5c37f0..f0bc936a7d3 100644 --- a/spec/javascripts/clusters/clusters_index_spec.js +++ b/spec/javascripts/clusters/clusters_index_spec.js @@ -1,10 +1,17 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import ClusterTable from '~/clusters/clusters_index'; +import { setTimeout } from 'core-js/library/web/timers'; describe('Clusters table', () => { + preloadFixtures('clusters/index_cluster.html.raw'); let ClustersClass; + let mock; beforeEach(() => { + loadFixtures('clusters/index_cluster.html.raw'); ClustersClass = new ClusterTable(); + mock = new MockAdapter(axios); }); afterEach(() => { @@ -13,19 +20,44 @@ describe('Clusters table', () => { describe('update cluster', () => { it('renders a toggle button', () => { - + expect(document.querySelector('.js-toggle-cluster-list')).not.toBeNull(); }); 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.classList).toContain('disabled'); + }); + afterEach(() => { + mock.restore(); }); - it('shows updated state after sucessfull request', () => { + it('shows updated state after sucessfull request', (done) => { + mock.onPut().reply(200, {}, {}); + const button = document.querySelector('.js-toggle-cluster-list'); + + button.click(); + setTimeout(() => { + expect(button.classList).toContain('is-loading'); + done(); + }, 0); }); - it('shows inital state after failed request', () => { + it('shows inital state after failed request', (done) => { + mock.onPut().reply(500, {}, {}); + const button = document.querySelector('.js-toggle-cluster-list'); + + button.click(); + setTimeout(() => { + expect(button.classList).toContain('is-loading'); + done(); + }, 0); }); }); }); diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index 8e74c4f859c..ea1b5d90f9f 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -31,4 +31,13 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle expect(response).to be_success store_frontend_fixture(response, example.description) end + + it 'clusters/index_cluster.html.raw' do |example| + get :index, + namespace_id: project.namespace.to_param, + project_id: project + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end end |