Commit 1b40a9f8 authored by Filipa Lacerda's avatar Filipa Lacerda

Handle toggle button with API request

Add tests
Update empty state
[ci skip]
parent 54cf7dac
...@@ -47,9 +47,15 @@ export default class ClusterTable { ...@@ -47,9 +47,15 @@ export default class ClusterTable {
* @param {HTMLElement} button * @param {HTMLElement} button
*/ */
static toggleLoadingButton(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('disabled');
button.classList.toggle('loading'); button.classList.toggle('is-loading');
button.querySelector('.loading-icon').classList.toggle('hidden');
} }
/** /**
......
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
@import "framework/tabs"; @import "framework/tabs";
@import "framework/timeline"; @import "framework/timeline";
@import "framework/tooltips"; @import "framework/tooltips";
@import "framework/toggle";
@import "framework/typography"; @import "framework/typography";
@import "framework/zen"; @import "framework/zen";
@import "framework/blank"; @import "framework/blank";
......
...@@ -348,3 +348,12 @@ ...@@ -348,3 +348,12 @@
} }
} }
} }
.flex-container-block {
display: -webkit-flex;
display: flex;
}
.flex-right {
margin-left: auto;
}
...@@ -454,11 +454,3 @@ img.emoji { ...@@ -454,11 +454,3 @@ img.emoji {
.inline { display: inline-block; } .inline { display: inline-block; }
.center { text-align: center; } .center { text-align: center; }
.vertical-align-middle { vertical-align: middle; } .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;
}
/**
* 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; }
}
}
...@@ -126,93 +126,6 @@ ...@@ -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, .project-home-panel,
.group-home-panel { .group-home-panel {
padding-top: 24px; padding-top: 24px;
......
...@@ -8,6 +8,8 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -8,6 +8,8 @@ class Projects::ClustersController < Projects::ApplicationController
def index def index
@clusters ||= project.clusters.page(params[:page]).per(20).map { |cluster| cluster.present(current_user: current_user) } @clusters ||= project.clusters.page(params[:page]).per(20).map { |cluster| cluster.present(current_user: current_user) }
@clusters_count = @clusters.count
end end
def login def login
......
.empty-state.flex-justify-content-center.flex-container-block.flex-wrap .row.empty-state
%div .col-xs-12
%h2= s_('ClusterIntegration|Integrate cluster automation') .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') - 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= 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 %p
= 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', title: 'Add cluster'
.svg-content
= image_tag 'illustrations/labels.svg'
...@@ -2,26 +2,26 @@ ...@@ -2,26 +2,26 @@
= render "empty_state" = render "empty_state"
- else - else
.top-area.scrolling-tabs-container.inner-page-scroll-tabs .top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left') .fade-left= icon("angle-left")
.fade-right= icon('angle-right') .fade-right= icon("angle-right")
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs
%li %li
%a %a.js-active-tab
= s_('ClusterIntegration|Active') =s_("ClusterIntegration|Active")
%span.badge %span.badge
0 TODO
%li %li
%a %a.js-inactive-tab
= s_('ClusterIntegration|Inactive') = s_("ClusterIntegration|Inactive")
%span.badge %span.badge
0 TODO
%li %li
%a %a.js-all-tab
= s_('ClusterIntegration|All') = s_("ClusterIntegration|All")
%span.badge %span.badge= @clusters_count
0
.nav-controls .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 .ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: 'row' } .gl-responsive-table-row.table-row-header{ role: 'row' }
.table-section.section-30{ role: 'rowheader' } .table-section.section-30{ role: 'rowheader' }
...@@ -35,17 +35,17 @@ ...@@ -35,17 +35,17 @@
.gl-responsive-table-row .gl-responsive-table-row
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: 'rowheader' }= s_('ClusterIntegration|Cluster') .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-section.section-30
.table-mobile-header{ role: 'rowheader' } .table-mobile-header{ role: 'rowheader' }
= s_('ClusterIntegration|Environment pattern') = s_('ClusterIntegration|Environment pattern')
.table-mobile-content .table-mobile-content= cluster.environment_scope
Content goes here
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: 'rowheader' } .table-mobile-header{ role: 'rowheader' }
= s_('ClusterIntegration|Project namespace') = s_('ClusterIntegration|Project namespace')
.table-mobile-content .table-mobile-content
Content goes here Content goes here - TODO
.table-section.section-10 .table-section.section-10
.table-mobile-header{ role: 'rowheader' } .table-mobile-header{ role: 'rowheader' }
.table-mobile-content .table-mobile-content
...@@ -56,5 +56,5 @@ ...@@ -56,5 +56,5 @@
data: { 'enabled-text': 'Enabled', data: { 'enabled-text': 'Enabled',
'disabled-text': 'Disabled', 'disabled-text': 'Disabled',
endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
= icon('loading', class: 'hidden') = icon('spinner spin', class: 'hidden loading-icon')
...@@ -95,23 +95,32 @@ feature 'Clusters', :js do ...@@ -95,23 +95,32 @@ feature 'Clusters', :js do
end end
it 'user sees a table with one cluster' do it 'user sees a table with one cluster' do
expect(page).to have_selector('.gl-responsive-table-row', count: 2)
end end
it 'user sees a disabled add cluster button ' do it 'user sees a disabled add cluster button ' do
expect(page.find(:css, '.js-add-cluster')['disabled']).to eq('true')
end end
it 'user sees navigation tabs' do 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 end
context 'update cluster' do context 'update cluster' do
it 'user can update cluster' do it 'user can update cluster' do
expect(page).to have_selector('.js-toggle-cluster-list')
end end
context 'with sucessfull request' do context 'with sucessfull request' do
it 'user sees updated cluster' do it 'user sees updated cluster' do
end end
end end
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import ClusterTable from '~/clusters/clusters_index'; import ClusterTable from '~/clusters/clusters_index';
import { setTimeout } from 'core-js/library/web/timers';
describe('Clusters table', () => { describe('Clusters table', () => {
preloadFixtures('clusters/index_cluster.html.raw');
let ClustersClass; let ClustersClass;
let mock;
beforeEach(() => { beforeEach(() => {
loadFixtures('clusters/index_cluster.html.raw');
ClustersClass = new ClusterTable(); ClustersClass = new ClusterTable();
mock = new MockAdapter(axios);
}); });
afterEach(() => { afterEach(() => {
...@@ -13,19 +20,44 @@ describe('Clusters table', () => { ...@@ -13,19 +20,44 @@ describe('Clusters table', () => {
describe('update cluster', () => { describe('update cluster', () => {
it('renders a toggle button', () => { it('renders a toggle button', () => {
expect(document.querySelector('.js-toggle-cluster-list')).not.toBeNull();
}); });
it('renders loading state while request is made', () => { 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');
}); });
it('shows updated state after sucessfull request', () => { 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();
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);
}); });
}); });
}); });
...@@ -31,4 +31,13 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle ...@@ -31,4 +31,13 @@ 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
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 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