Commit 42ceab4f authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '207031-cluster-list-backend' into 'master'

Add endpoint to clusterList Vue component

See merge request gitlab-org/gitlab!31013
parents 084f9a78 06fb70fc
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { GlTable, GlLoadingIcon, GlBadge } from '@gitlab/ui'; import { GlTable, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { CLUSTER_TYPES, STATUSES } from '../constants'; import { CLUSTER_TYPES, STATUSES } from '../constants';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
...@@ -8,54 +8,58 @@ import { __, sprintf } from '~/locale'; ...@@ -8,54 +8,58 @@ import { __, sprintf } from '~/locale';
export default { export default {
components: { components: {
GlTable, GlTable,
GlLink,
GlLoadingIcon, GlLoadingIcon,
GlBadge, GlBadge,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
fields: [
{
key: 'name',
label: __('Kubernetes cluster'),
},
{
key: 'environmentScope',
label: __('Environment scope'),
},
{
key: 'size',
label: __('Size'),
},
{
key: 'cpu',
label: __('Total cores (vCPUs)'),
},
{
key: 'memory',
label: __('Total memory (GB)'),
},
{
key: 'clusterType',
label: __('Cluster level'),
formatter: value => CLUSTER_TYPES[value],
},
],
computed: { computed: {
...mapState(['clusters', 'loading']), ...mapState(['clusters', 'loading']),
fields() {
return [
{
key: 'name',
label: __('Kubernetes cluster'),
},
{
key: 'environment_scope',
label: __('Environment scope'),
},
// Wait for backend to send these fields
// {
// key: 'size',
// label: __('Size'),
// },
// {
// key: 'cpu',
// label: __('Total cores (vCPUs)'),
// },
// {
// key: 'memory',
// label: __('Total memory (GB)'),
// },
{
key: 'cluster_type',
label: __('Cluster level'),
formatter: value => CLUSTER_TYPES[value],
},
];
},
}, },
mounted() { mounted() {
// TODO - uncomment this once integrated with BE this.fetchClusters();
// this.fetchClusters();
}, },
methods: { methods: {
...mapActions(['fetchClusters']), ...mapActions(['fetchClusters']),
statusClass(status) { statusClass(status) {
return STATUSES[status].className; const iconClass = STATUSES[status] || STATUSES.default;
return iconClass.className;
}, },
statusTitle(status) { statusTitle(status) {
const { title } = STATUSES[status]; const iconTitle = STATUSES[status] || STATUSES.default;
return sprintf(__('Status: %{title}'), { title }, false); return sprintf(__('Status: %{title}'), { title: iconTitle.title }, false);
}, },
}, },
}; };
...@@ -63,17 +67,13 @@ export default { ...@@ -63,17 +67,13 @@ export default {
<template> <template>
<gl-loading-icon v-if="loading" size="md" class="mt-3" /> <gl-loading-icon v-if="loading" size="md" class="mt-3" />
<gl-table <gl-table v-else :items="clusters" :fields="fields" stacked="md" class="qa-clusters-table">
v-else
:items="clusters"
:fields="$options.fields"
stacked="md"
variant="light"
class="qa-clusters-table"
>
<template #cell(name)="{ item }"> <template #cell(name)="{ item }">
<div class="d-flex flex-row-reverse flex-md-row js-status"> <div class="d-flex flex-row-reverse flex-md-row js-status">
{{ item.name }} <gl-link data-qa-selector="cluster" :data-qa-cluster-name="item.name" :href="item.path">
{{ item.name }}
</gl-link>
<gl-loading-icon <gl-loading-icon
v-if="item.status === 'deleting'" v-if="item.status === 'deleting'"
v-tooltip v-tooltip
...@@ -84,13 +84,13 @@ export default { ...@@ -84,13 +84,13 @@ export default {
<div <div
v-else v-else
v-tooltip v-tooltip
class="cluster-status-indicator rounded-circle align-self-center gl-w-8 gl-h-8 mr-2 ml-md-2" class="cluster-status-indicator rounded-circle align-self-center gl-w-4 gl-h-4 mr-2 ml-md-2"
:class="statusClass(item.status)" :class="statusClass(item.status)"
:title="statusTitle(item.status)" :title="statusTitle(item.status)"
></div> ></div>
</div> </div>
</template> </template>
<template #cell(clusterType)="{value}"> <template #cell(cluster_type)="{value}">
<gl-badge variant="light"> <gl-badge variant="light">
{{ value }} {{ value }}
</gl-badge> </gl-badge>
......
...@@ -7,8 +7,9 @@ export const CLUSTER_TYPES = { ...@@ -7,8 +7,9 @@ export const CLUSTER_TYPES = {
}; };
export const STATUSES = { export const STATUSES = {
default: { className: 'bg-white', title: __('Unknown') },
disabled: { className: 'disabled', title: __('Disabled') }, disabled: { className: 'disabled', title: __('Disabled') },
connected: { className: 'bg-success', title: __('Connected') }, created: { className: 'bg-success', title: __('Connected') },
unreachable: { className: 'bg-danger', title: __('Unreachable') }, unreachable: { className: 'bg-danger', title: __('Unreachable') },
authentication_failure: { className: 'bg-warning', title: __('Authentication Failure') }, authentication_failure: { className: 'bg-warning', title: __('Authentication Failure') },
deleting: { title: __('Deleting') }, deleting: { title: __('Deleting') },
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll'; import Poll from '~/lib/utils/poll';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Visibility from 'visibilityjs';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
...@@ -14,23 +12,16 @@ export const fetchClusters = ({ state, commit }) => { ...@@ -14,23 +12,16 @@ export const fetchClusters = ({ state, commit }) => {
data: state.endpoint, data: state.endpoint,
method: 'fetchClusters', method: 'fetchClusters',
successCallback: ({ data }) => { successCallback: ({ data }) => {
commit(types.SET_CLUSTERS_DATA, convertObjectPropsToCamelCase(data, { deep: true })); if (data.clusters) {
commit(types.SET_LOADING_STATE, false); commit(types.SET_CLUSTERS_DATA, data);
commit(types.SET_LOADING_STATE, false);
poll.stop();
}
}, },
errorCallback: () => flash(__('An error occurred while loading clusters')), errorCallback: () => flash(__('An error occurred while loading clusters')),
}); });
if (!Visibility.hidden()) { poll.makeRequest();
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
......
...@@ -4,9 +4,10 @@ export default { ...@@ -4,9 +4,10 @@ export default {
[types.SET_LOADING_STATE](state, value) { [types.SET_LOADING_STATE](state, value) {
state.loading = value; state.loading = value;
}, },
[types.SET_CLUSTERS_DATA](state, clusters) { [types.SET_CLUSTERS_DATA](state, data) {
Object.assign(state, { Object.assign(state, {
clusters, clusters: data.clusters,
hasAncestorClusters: data.has_ancestor_clusters,
}); });
}, },
}; };
export default (initialState = {}) => ({ export default (initialState = {}) => ({
endpoint: initialState.endpoint, endpoint: initialState.endpoint,
loading: false, // TODO - set this to true once integrated with BE hasAncestorClusters: false,
loading: true,
clusters: [], clusters: [],
}); });
...@@ -21,8 +21,8 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated ...@@ -21,8 +21,8 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
can?(current_user, :create_cluster, clusterable) can?(current_user, :create_cluster, clusterable)
end end
def index_path def index_path(options = {})
polymorphic_path([clusterable, :clusters]) polymorphic_path([clusterable, :clusters], options)
end end
def new_path(options = {}) def new_path(options = {})
......
...@@ -13,8 +13,8 @@ class InstanceClusterablePresenter < ClusterablePresenter ...@@ -13,8 +13,8 @@ class InstanceClusterablePresenter < ClusterablePresenter
end end
override :index_path override :index_path
def index_path def index_path(options = {})
admin_clusters_path admin_clusters_path(options)
end end
override :new_path override :new_path
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
= link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence') = link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence')
- if Feature.enabled?(:clusters_list_redesign) - if Feature.enabled?(:clusters_list_redesign)
#js-clusters-list-app{ data: { endpoint: 'todo/add/endpoint' } } #js-clusters-list-app{ data: { endpoint: clusterable.index_path(format: :json) } }
- else - else
.clusters-table.js-clusters-list .clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" } .gl-responsive-table-row.table-row-header{ role: "row" }
......
...@@ -22278,15 +22278,9 @@ msgstr "" ...@@ -22278,15 +22278,9 @@ msgstr ""
msgid "Total artifacts size: %{total_size}" msgid "Total artifacts size: %{total_size}"
msgstr "" msgstr ""
msgid "Total cores (vCPUs)"
msgstr ""
msgid "Total issues" msgid "Total issues"
msgstr "" msgstr ""
msgid "Total memory (GB)"
msgstr ""
msgid "Total test time for all commits/merges" msgid "Total test time for all commits/merges"
msgstr "" msgstr ""
......
import Vuex from 'vuex'; import axios from '~/lib/utils/axios_utils';
import { createLocalVue, mount } from '@vue/test-utils';
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
import Clusters from '~/clusters_list/components/clusters.vue'; import Clusters from '~/clusters_list/components/clusters.vue';
import mockData from '../mock_data'; import ClusterStore from '~/clusters_list/store';
import MockAdapter from 'axios-mock-adapter';
const localVue = createLocalVue(); import { apiData } from '../mock_data';
localVue.use(Vuex); import { mount } from '@vue/test-utils';
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
describe('Clusters', () => { describe('Clusters', () => {
let mock;
let store;
let wrapper; let wrapper;
const findTable = () => wrapper.find(GlTable); const endpoint = 'some/endpoint';
const findLoader = () => wrapper.find(GlLoadingIcon); const findLoader = () => wrapper.find(GlLoadingIcon);
const findTable = () => wrapper.find(GlTable);
const findStatuses = () => findTable().findAll('.js-status'); const findStatuses = () => findTable().findAll('.js-status');
const mountComponent = _state => { const mockPollingApi = (response, body, header) => {
const state = { clusters: mockData, endpoint: 'some/endpoint', ..._state }; mock.onGet(endpoint).reply(response, body, header);
const store = new Vuex.Store({ };
state,
});
wrapper = mount(Clusters, { localVue, store }); const mountWrapper = () => {
store = ClusterStore({ endpoint });
wrapper = mount(Clusters, { store });
return axios.waitForAll();
}; };
beforeEach(() => { beforeEach(() => {
mountComponent({ loading: false }); mock = new MockAdapter(axios);
mockPollingApi(200, apiData, {});
return mountWrapper();
});
afterEach(() => {
wrapper.destroy();
mock.restore();
}); });
describe('clusters table', () => { describe('clusters table', () => {
it('displays a loader instead of the table while loading', () => { describe('when data is loading', () => {
mountComponent({ loading: true }); beforeEach(() => {
expect(findLoader().exists()).toBe(true); wrapper.vm.$store.state.loading = true;
expect(findTable().exists()).toBe(false); return wrapper.vm.$nextTick();
});
it('displays a loader instead of the table while loading', () => {
expect(findLoader().exists()).toBe(true);
expect(findTable().exists()).toBe(false);
});
}); });
it('displays a table component', () => { it('displays a table component', () => {
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findTable().exists()).toBe(true);
}); });
it('renders the correct table headers', () => { it('renders the correct table headers', () => {
const tableHeaders = wrapper.vm.$options.fields; const tableHeaders = wrapper.vm.fields;
const headers = findTable().findAll('th'); const headers = findTable().findAll('th');
expect(headers.length).toBe(tableHeaders.length); expect(headers.length).toBe(tableHeaders.length);
...@@ -62,7 +79,8 @@ describe('Clusters', () => { ...@@ -62,7 +79,8 @@ describe('Clusters', () => {
${'unreachable'} | ${'bg-danger'} | ${1} ${'unreachable'} | ${'bg-danger'} | ${1}
${'authentication_failure'} | ${'bg-warning'} | ${2} ${'authentication_failure'} | ${'bg-warning'} | ${2}
${'deleting'} | ${null} | ${3} ${'deleting'} | ${null} | ${3}
${'connected'} | ${'bg-success'} | ${4} ${'created'} | ${'bg-success'} | ${4}
${'default'} | ${'bg-white'} | ${5}
`('renders a status for each cluster', ({ statusName, className, lineNumber }) => { `('renders a status for each cluster', ({ statusName, className, lineNumber }) => {
const statuses = findStatuses(); const statuses = findStatuses();
const status = statuses.at(lineNumber); const status = statuses.at(lineNumber);
......
export default [ export const clusterList = [
{ {
name: 'My Cluster 1', name: 'My Cluster 1',
environmentScope: '*', environmentScope: '*',
...@@ -40,8 +40,22 @@ export default [ ...@@ -40,8 +40,22 @@ export default [
environmentScope: 'development', environmentScope: 'development',
size: '12', size: '12',
clusterType: 'project_type', clusterType: 'project_type',
status: 'connected', status: 'created',
cpu: '6 (100% free)',
memory: '20.12 (35% free)',
},
{
name: 'My Cluster 6',
environmentScope: '*',
size: '1',
clusterType: 'project_type',
status: 'cleanup_ongoing',
cpu: '6 (100% free)', cpu: '6 (100% free)',
memory: '20.12 (35% free)', memory: '20.12 (35% free)',
}, },
]; ];
export const apiData = {
clusters: clusterList,
has_ancestor_clusters: false,
};
...@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import flashError from '~/flash'; import flashError from '~/flash';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { apiData } from '../mock_data';
import * as types from '~/clusters_list/store/mutation_types'; import * as types from '~/clusters_list/store/mutation_types';
import * as actions from '~/clusters_list/store/actions'; import * as actions from '~/clusters_list/store/actions';
...@@ -10,8 +11,6 @@ jest.mock('~/flash.js'); ...@@ -10,8 +11,6 @@ jest.mock('~/flash.js');
describe('Clusters store actions', () => { describe('Clusters store actions', () => {
describe('fetchClusters', () => { describe('fetchClusters', () => {
let mock; let mock;
const endpoint = '/clusters';
const clusters = [{ name: 'test' }];
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
...@@ -20,14 +19,14 @@ describe('Clusters store actions', () => { ...@@ -20,14 +19,14 @@ describe('Clusters store actions', () => {
afterEach(() => mock.restore()); afterEach(() => mock.restore());
it('should commit SET_CLUSTERS_DATA with received response', done => { it('should commit SET_CLUSTERS_DATA with received response', done => {
mock.onGet().reply(200, clusters); mock.onGet().reply(200, apiData);
testAction( testAction(
actions.fetchClusters, actions.fetchClusters,
{ endpoint }, { endpoint: apiData.endpoint },
{}, {},
[ [
{ type: types.SET_CLUSTERS_DATA, payload: clusters }, { type: types.SET_CLUSTERS_DATA, payload: apiData },
{ type: types.SET_LOADING_STATE, payload: false }, { type: types.SET_LOADING_STATE, payload: false },
], ],
[], [],
...@@ -38,7 +37,7 @@ describe('Clusters store actions', () => { ...@@ -38,7 +37,7 @@ describe('Clusters store actions', () => {
it('should show flash on API error', done => { it('should show flash on API error', done => {
mock.onGet().reply(400, 'Not Found'); mock.onGet().reply(400, 'Not Found');
testAction(actions.fetchClusters, { endpoint }, {}, [], [], () => { testAction(actions.fetchClusters, { endpoint: apiData.endpoint }, {}, [], [], () => {
expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error')); expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
done(); done();
}); });
......
...@@ -87,4 +87,20 @@ describe ClusterablePresenter do ...@@ -87,4 +87,20 @@ describe ClusterablePresenter do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
describe '#index_path' do
let(:clusterable) { create(:group) }
context 'without options' do
subject { described_class.new(clusterable).index_path }
it { is_expected.to eq(group_clusters_path(clusterable)) }
end
context 'with options' do
subject { described_class.new(clusterable).index_path(format: :json) }
it { is_expected.to eq(group_clusters_path(clusterable, format: :json)) }
end
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