Commit b28bf503 authored by anna_vovchenko's avatar anna_vovchenko Committed by Nicolò Maria Mezzopera

Moved clusters empty state to Vue component

As we want to change the Kubernetes section UX,
we need to have all related components in Vue.

Changelog: changed
parent 93310098
......@@ -14,6 +14,7 @@ import { __, sprintf } from '~/locale';
import { CLUSTER_TYPES, STATUSES } from '../constants';
import AncestorNotice from './ancestor_notice.vue';
import NodeErrorHelpText from './node_error_help_text.vue';
import ClustersEmptyState from './clusters_empty_state.vue';
export default {
nodeMemoryText: __('%{totalMemory} (%{freeSpacePercentage}%{percentSymbol} free)'),
......@@ -28,6 +29,7 @@ export default {
GlSprintf,
GlTable,
NodeErrorHelpText,
ClustersEmptyState,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -40,7 +42,7 @@ export default {
'loadingNodes',
'page',
'providers',
'totalCulsters',
'totalClusters',
]),
contentAlignClasses() {
return 'gl-display-flex gl-align-items-center gl-justify-content-end gl-justify-content-md-start';
......@@ -83,9 +85,12 @@ export default {
},
];
},
hasClusters() {
hasClustersPerPage() {
return this.clustersPerPage > 0;
},
hasClusters() {
return this.totalClusters > 0;
},
},
mounted() {
this.fetchClusters();
......@@ -202,6 +207,7 @@ export default {
<ancestor-notice />
<gl-table
v-if="hasClusters"
:items="clusters"
:fields="fields"
stacked="md"
......@@ -298,11 +304,13 @@ export default {
</template>
</gl-table>
<ClustersEmptyState v-else />
<gl-pagination
v-if="hasClusters"
v-if="hasClustersPerPage"
v-model="currentPage"
:per-page="clustersPerPage"
:total-items="totalCulsters"
:total-items="totalClusters"
:prev-text="__('Prev')"
:next-text="__('Next')"
align="center"
......
<script>
import { GlEmptyState, GlButton, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
components: {
GlEmptyState,
GlButton,
GlLink,
},
inject: ['emptyStateHelpText', 'clustersEmptyStateImage', 'newClusterPath'],
learnMoreHelpUrl: helpPagePath('user/project/clusters/index'),
computed: {
...mapState(['canAddCluster']),
},
};
</script>
<template>
<gl-empty-state
:svg-path="clustersEmptyStateImage"
:title="s__('ClusterIntegration|Integrate Kubernetes with a cluster certificate')"
>
<template #description>
<p class="mw-460 gl-mx-auto gl-text-left">
{{
s__(
'ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.',
)
}}
</p>
<p
v-if="emptyStateHelpText"
class="mw-460 gl-mx-auto gl-text-left"
data-testid="clusters-empty-state-text"
>
{{ emptyStateHelpText }}
</p>
<p class="mw-460 gl-mx-auto">
<gl-link :href="$options.learnMoreHelpUrl" target="_blank" data-testid="clusters-docs-link">
{{ s__('ClusterIntegration|Learn more about Kubernetes') }}
</gl-link>
</p>
</template>
<template #actions>
<gl-button
data-testid="integration-primary-button"
data-qa-selector="add_kubernetes_cluster_link"
category="primary"
variant="confirm"
:disabled="!canAddCluster"
:href="newClusterPath"
>
{{ s__('ClusterIntegration|Integrate with a cluster certificate') }}
</gl-button>
</template>
</gl-empty-state>
</template>
......@@ -8,8 +8,15 @@ export default (Vue) => {
return null;
}
const { emptyStateHelpText, newClusterPath, clustersEmptyStateImage } = el.dataset;
return new Vue({
el,
provide: {
emptyStateHelpText,
newClusterPath,
clustersEmptyStateImage,
},
store: createStore(el.dataset),
render(createElement) {
return createElement(Clusters);
......
......@@ -12,7 +12,7 @@ export default {
clusters: data.clusters,
clustersPerPage: paginationInformation.perPage,
hasAncestorClusters: data.has_ancestor_clusters,
totalCulsters: paginationInformation.total,
totalClusters: paginationInformation.total,
});
},
[types.SET_PAGE](state, value) {
......
import { parseBoolean } from '~/lib/utils/common_utils';
export default (initialState = {}) => ({
ancestorHelperPath: initialState.ancestorHelpPath,
endpoint: initialState.endpoint,
......@@ -12,5 +14,6 @@ export default (initialState = {}) => ({
default: { path: initialState.imgTagsDefaultPath, text: initialState.imgTagsDefaultText },
gcp: { path: initialState.imgTagsGcpPath, text: initialState.imgTagsGcpText },
},
totalCulsters: 0,
totalClusters: 0,
canAddCluster: parseBoolean(initialState.canAddCluster),
});
......@@ -29,15 +29,19 @@ module ClustersHelper
}
end
def js_clusters_list_data(path = nil)
def js_clusters_list_data(clusterable)
{
ancestor_help_path: help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'),
endpoint: path,
endpoint: clusterable.index_path(format: :json),
img_tags: {
aws: { path: image_path('illustrations/logos/amazon_eks.svg'), text: s_('ClusterIntegration|Amazon EKS') },
default: { path: image_path('illustrations/logos/kubernetes.svg'), text: _('Kubernetes Cluster') },
gcp: { path: image_path('illustrations/logos/google_gke.svg'), text: s_('ClusterIntegration|Google GKE') }
}
},
clusters_empty_state_image: image_path('illustrations/clusters_empty.svg'),
empty_state_help_text: clusterable.empty_state_help_text,
new_cluster_path: clusterable.new_path(tab: 'create'),
can_add_cluster: clusterable.can_add_cluster?.to_s
}
end
......
- if clusters.empty?
= render 'empty_state'
- else
.top-area.adjust
.top-area.adjust
.gl-display-block.gl-text-right.gl-my-4.gl-w-full
- if clusterable.can_add_cluster?
= link_to s_('ClusterIntegration|Connect cluster with certificate'), clusterable.new_path, class: 'btn gl-button btn-confirm js-add-cluster gl-py-2', qa_selector: :integrate_kubernetes_cluster_button
......@@ -9,4 +6,4 @@
%span.btn.gl-button.btn-confirm.js-add-cluster.disabled.gl-py-2
= s_("ClusterIntegration|Connect cluster with certificate")
#js-clusters-list-app{ data: js_clusters_list_data(clusterable.index_path(format: :json)) }
#js-clusters-list-app{ data: js_clusters_list_data(clusterable) }
.row.empty-state
.col-12
.svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-12
.text-content
%h4.gl-text-center= s_('ClusterIntegration|Integrate Kubernetes with a cluster certificate')
%p.gl-text-center
= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
= clusterable.empty_state_help_text
= clusterable.learn_more_link
- if clusterable.can_add_cluster?
.gl-text-center
= link_to s_('ClusterIntegration|Integrate with a cluster certificate'), clusterable.new_path, class: 'gl-button btn btn-confirm', data: { qa_selector: 'add_kubernetes_cluster_link' }
......@@ -6,7 +6,7 @@ module QA
module Infrastructure
module Kubernetes
class Index < Page::Base
view 'app/views/clusters/clusters/_empty_state.html.haml' do
view 'app/assets/javascripts/clusters_list/components/clusters_empty_state.vue' do
element :add_kubernetes_cluster_link
end
......
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
const clustersEmptyStateImage = 'path/to/svg';
const newClusterPath = '/path/to/connect/cluster';
const emptyStateHelpText = 'empty state text';
const canAddCluster = true;
describe('ClustersEmptyStateComponent', () => {
let wrapper;
const propsData = {
childComponent: false,
};
const provideData = {
clustersEmptyStateImage,
emptyStateHelpText: null,
newClusterPath,
};
const entryData = {
canAddCluster,
};
const findButton = () => wrapper.findComponent(GlButton);
const findEmptyStateText = () => wrapper.findByTestId('clusters-empty-state-text');
beforeEach(() => {
wrapper = shallowMountExtended(ClustersEmptyState, {
store: ClusterStore(entryData),
propsData,
provide: provideData,
stubs: { GlEmptyState },
});
});
afterEach(() => {
wrapper.destroy();
});
it('should render the action button', () => {
expect(findButton().exists()).toBe(true);
});
describe('when the help text is not provided', () => {
it('should not render the empty state text', () => {
expect(findEmptyStateText().exists()).toBe(false);
});
});
describe('when the help text is provided', () => {
beforeEach(() => {
provideData.emptyStateHelpText = emptyStateHelpText;
wrapper = shallowMountExtended(ClustersEmptyState, {
store: ClusterStore(entryData),
propsData,
provide: provideData,
});
});
it('should show the empty state text', () => {
expect(findEmptyStateText().exists()).toBe(true);
expect(findEmptyStateText().text()).toBe(emptyStateHelpText);
});
});
describe('when the user cannot add clusters', () => {
beforeEach(() => {
wrapper.vm.$store.state.canAddCluster = false;
});
it('should disable the button', () => {
expect(findButton().props('disabled')).toBe(true);
});
});
});
......@@ -8,6 +8,7 @@ import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
import axios from '~/lib/utils/axios_utils';
import { apiData } from '../mock_data';
......@@ -18,18 +19,30 @@ describe('Clusters', () => {
let wrapper;
const endpoint = 'some/endpoint';
const totalClustersNumber = 6;
const clustersEmptyStateImage = 'path/to/svg';
const emptyStateHelpText = null;
const newClusterPath = '/path/to/new/cluster';
const entryData = {
endpoint,
imgTagsAwsText: 'AWS Icon',
imgTagsDefaultText: 'Default Icon',
imgTagsGcpText: 'GCP Icon',
totalClusters: totalClustersNumber,
};
const findLoader = () => wrapper.find(GlLoadingIcon);
const findPaginatedButtons = () => wrapper.find(GlPagination);
const findTable = () => wrapper.find(GlTable);
const provideData = {
clustersEmptyStateImage,
emptyStateHelpText,
newClusterPath,
};
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
const findPaginatedButtons = () => wrapper.findComponent(GlPagination);
const findTable = () => wrapper.findComponent(GlTable);
const findStatuses = () => findTable().findAll('.js-status');
const findEmptyState = () => wrapper.findComponent(ClustersEmptyState);
const mockPollingApi = (response, body, header) => {
mock.onGet(`${endpoint}?page=${header['x-page']}`).reply(response, body, header);
......@@ -37,7 +50,7 @@ describe('Clusters', () => {
const mountWrapper = () => {
store = ClusterStore(entryData);
wrapper = mount(Clusters, { store });
wrapper = mount(Clusters, { provide: provideData, store, stubs: { GlTable } });
return axios.waitForAll();
};
......@@ -70,7 +83,6 @@ describe('Clusters', () => {
describe('when data is loading', () => {
beforeEach(() => {
wrapper.vm.$store.state.loadingClusters = true;
return wrapper.vm.$nextTick();
});
it('displays a loader instead of the table while loading', () => {
......@@ -79,6 +91,7 @@ describe('Clusters', () => {
});
});
describe('when clusters are present', () => {
it('displays a table component', () => {
expect(findTable().exists()).toBe(true);
});
......@@ -99,6 +112,16 @@ describe('Clusters', () => {
});
});
describe('when there are no clusters', () => {
beforeEach(() => {
wrapper.vm.$store.state.totalClusters = 0;
});
it('should render empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
});
});
describe('cluster icon', () => {
it.each`
providerText | lineNumber
......
......@@ -26,7 +26,7 @@ describe('Admin statistics panel mutations', () => {
expect(state.clusters).toBe(apiData.clusters);
expect(state.clustersPerPage).toBe(paginationInformation.perPage);
expect(state.hasAncestorClusters).toBe(apiData.has_ancestor_clusters);
expect(state.totalCulsters).toBe(paginationInformation.total);
expect(state.totalClusters).toBe(paginationInformation.total);
});
});
......
......@@ -89,10 +89,14 @@ RSpec.describe ClustersHelper do
end
describe '#js_clusters_list_data' do
subject { helper.js_clusters_list_data('/path') }
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { build(:project) }
let_it_be(:clusterable) { ClusterablePresenter.fabricate(project, current_user: current_user) }
subject { helper.js_clusters_list_data(clusterable) }
it 'displays endpoint path' do
expect(subject[:endpoint]).to eq('/path')
expect(subject[:endpoint]).to eq(clusterable.index_path(format: :json))
end
it 'generates svg image data', :aggregate_failures do
......@@ -108,6 +112,22 @@ RSpec.describe ClustersHelper do
it 'displays and ancestor_help_path' do
expect(subject[:ancestor_help_path]).to eq(help_page_path('user/group/clusters/index', anchor: 'cluster-precedence'))
end
it 'displays empty image path' do
expect(subject[:clusters_empty_state_image]).to match(%r(/illustrations/logos/clusters_empty|svg))
end
it 'displays empty state help text' do
expect(subject[:empty_state_help_text]).to match(clusterable.empty_state_help_text)
end
it 'displays create cluster using certificate path' do
expect(subject[:new_cluster_path]).to match(clusterable.new_path(tab: 'create'))
end
it 'displays whether the user can add cluster' do
expect(subject[:can_add_cluster]).to match(clusterable.can_add_cluster?.to_s)
end
end
describe '#js_cluster_new' do
......
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