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