mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-08-20 14:11:11 +00:00
Handle toggle button with API request
Add tests Update empty state [ci skip]
This commit is contained in:
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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";
|
||||
|
@ -348,3 +348,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flex-container-block {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
139
app/assets/stylesheets/framework/toggle.scss
Normal file
139
app/assets/stylesheets/framework/toggle.scss
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
it('shows updated state after sucessfull request', () => {
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('shows inital state after failed 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', (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
|
||||
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
|
||||
|
Reference in New Issue
Block a user