Commit 9dad1acd authored by Jacques Erasmus's avatar Jacques Erasmus Committed by Kushal Pandya

Add clusters list skeleton

Added a Vue app for the Clusters List
parent ecabb9b8
<script>
import { mapState, mapActions } from 'vuex';
import { GlTable, GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { CLUSTER_TYPES } from '../constants';
import { __ } from '~/locale';
export default {
components: {
GlTable,
GlLoadingIcon,
GlBadge,
},
fields: [
{
key: 'name',
label: __('Kubernetes cluster'),
},
{
key: 'environmentScope',
label: __('Environment scope'),
},
{
key: 'size',
label: __('Size'),
},
{
key: 'clusterType',
label: __('Cluster level'),
formatter: value => CLUSTER_TYPES[value],
},
],
computed: {
...mapState(['clusters', 'loading']),
},
mounted() {
// TODO - uncomment this once integrated with BE
// this.fetchClusters();
},
methods: {
...mapActions(['fetchClusters']),
},
};
</script>
<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"
>
<template #cell(clusterType)="{value}">
<gl-badge variant="light">
{{ value }}
</gl-badge>
</template>
</gl-table>
</template>
import { __ } from '~/locale';
export const CLUSTER_TYPES = {
project_type: __('Project'),
group_type: __('Group'),
instance_type: __('Instance'),
};
export default {
CLUSTER_TYPES,
};
import Vue from 'vue';
import Clusters from './components/clusters.vue';
import { createStore } from './store';
export default () => {
const entryPoint = document.querySelector('#js-clusters-list-app');
if (!entryPoint) {
return;
}
const { endpoint } = entryPoint.dataset;
// eslint-disable-next-line no-new
new Vue({
el: '#js-clusters-list-app',
store: createStore({ endpoint }),
render(createElement) {
return createElement(Clusters);
},
});
};
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const fetchClusters = ({ state, commit }) => {
return axios
.get(state.endpoint)
.then(({ data }) => {
commit(types.SET_CLUSTERS_DATA, convertObjectPropsToCamelCase(data, { deep: true }));
commit(types.SET_LOADING_STATE, false);
})
.catch(() => flash(__('An error occurred while loading clusters')));
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import mutations from './mutations';
import * as actions from './actions';
Vue.use(Vuex);
export const createStore = initialState =>
new Vuex.Store({
actions,
mutations,
state: state(initialState),
});
export default createStore;
export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA';
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
import * as types from './mutation_types';
export default {
[types.SET_LOADING_STATE](state, value) {
state.loading = value;
},
[types.SET_CLUSTERS_DATA](state, clusters) {
Object.assign(state, {
clusters,
});
},
};
export default (initialState = {}) => ({
endpoint: initialState.endpoint,
loading: false, // TODO - set this to true once integrated with BE
clusters: [
// TODO - remove mock data once integrated with BE
// {
// name: 'My Cluster',
// environmentScope: '*',
// size: '3',
// clusterType: 'group_type',
// },
// {
// name: 'My other cluster',
// environmentScope: 'production',
// size: '12',
// clusterType: 'project_type',
// },
],
});
import PersistentUserCallout from '~/persistent_user_callout'; import PersistentUserCallout from '~/persistent_user_callout';
import initClustersListApp from '~/clusters_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.gcp-signup-offer'); const callout = document.querySelector('.gcp-signup-offer');
PersistentUserCallout.factory(callout); PersistentUserCallout.factory(callout);
initClustersListApp();
}); });
import PersistentUserCallout from '~/persistent_user_callout'; import PersistentUserCallout from '~/persistent_user_callout';
import initClustersListApp from '~/clusters_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.gcp-signup-offer'); const callout = document.querySelector('.gcp-signup-offer');
PersistentUserCallout.factory(callout); PersistentUserCallout.factory(callout);
initClustersListApp();
}); });
import PersistentUserCallout from '~/persistent_user_callout'; import PersistentUserCallout from '~/persistent_user_callout';
import initClustersListApp from '~/clusters_list';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.gcp-signup-offer'); const callout = document.querySelector('.gcp-signup-offer');
PersistentUserCallout.factory(callout); PersistentUserCallout.factory(callout);
initClustersListApp();
}); });
...@@ -18,13 +18,16 @@ ...@@ -18,13 +18,16 @@
%strong %strong
= 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')
.clusters-table.js-clusters-list - if Feature.enabled?(:clusters_list_redesign)
.gl-responsive-table-row.table-row-header{ role: "row" } #js-clusters-list-app{ data: { endpoint: 'todo/add/endpoint' } }
.table-section.section-60{ role: "rowheader" } - else
= s_("ClusterIntegration|Kubernetes cluster") .clusters-table.js-clusters-list
.table-section.section-30{ role: "rowheader" } .gl-responsive-table-row.table-row-header{ role: "row" }
= s_("ClusterIntegration|Environment scope") .table-section.section-60{ role: "rowheader" }
.table-section.section-10{ role: "rowheader" } = s_("ClusterIntegration|Kubernetes cluster")
- @clusters.each do |cluster| .table-section.section-30{ role: "rowheader" }
= render "cluster", cluster: cluster.present(current_user: current_user) = s_("ClusterIntegration|Environment scope")
= paginate @clusters, theme: "gitlab" .table-section.section-10{ role: "rowheader" }
- @clusters.each do |cluster|
= render "cluster", cluster: cluster.present(current_user: current_user)
= paginate @clusters, theme: "gitlab"
...@@ -11,6 +11,7 @@ describe 'EE Clusters', :js do ...@@ -11,6 +11,7 @@ describe 'EE Clusters', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
gitlab_sign_in(user) gitlab_sign_in(user)
stub_feature_flags(clusters_list_redesign: false)
end end
context 'when user has a cluster' do context 'when user has a cluster' do
......
...@@ -1862,6 +1862,9 @@ msgstr "" ...@@ -1862,6 +1862,9 @@ msgstr ""
msgid "An error occurred while loading chart data" msgid "An error occurred while loading chart data"
msgstr "" msgstr ""
msgid "An error occurred while loading clusters"
msgstr ""
msgid "An error occurred while loading commit signatures" msgid "An error occurred while loading commit signatures"
msgstr "" msgstr ""
...@@ -3962,6 +3965,9 @@ msgstr "" ...@@ -3962,6 +3965,9 @@ msgstr ""
msgid "Cluster does not exist" msgid "Cluster does not exist"
msgstr "" msgstr ""
msgid "Cluster level"
msgstr ""
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}." msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr "" msgstr ""
...@@ -7454,6 +7460,9 @@ msgstr "" ...@@ -7454,6 +7460,9 @@ msgstr ""
msgid "Environment does not have deployments" msgid "Environment does not have deployments"
msgstr "" msgstr ""
msgid "Environment scope"
msgstr ""
msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want." msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
msgstr "" msgstr ""
...@@ -11181,6 +11190,9 @@ msgstr "" ...@@ -11181,6 +11190,9 @@ msgstr ""
msgid "Kubernetes Clusters" msgid "Kubernetes Clusters"
msgstr "" msgstr ""
msgid "Kubernetes cluster"
msgstr ""
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}" msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
msgstr "" msgstr ""
......
...@@ -11,6 +11,7 @@ describe 'Clusters', :js do ...@@ -11,6 +11,7 @@ describe 'Clusters', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
gitlab_sign_in(user) gitlab_sign_in(user)
stub_feature_flags(clusters_list_redesign: false)
end end
context 'when user does not have a cluster and visits cluster index page' do context 'when user does not have a cluster and visits cluster index page' do
......
import { createLocalVue, mount } from '@vue/test-utils';
import { GlTable, GlLoadingIcon } from '@gitlab/ui';
import Clusters from '~/clusters_list/components/clusters.vue';
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Clusters', () => {
let wrapper;
const findTable = () => wrapper.find(GlTable);
const findLoader = () => wrapper.find(GlLoadingIcon);
const mountComponent = _state => {
const state = { clusters: [], endpoint: 'some/endpoint', ..._state };
const store = new Vuex.Store({
state,
});
wrapper = mount(Clusters, { localVue, store });
};
beforeEach(() => {
mountComponent({ loading: false });
});
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);
});
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 headers = findTable().findAll('th');
expect(headers.length).toBe(tableHeaders.length);
tableHeaders.forEach((headerText, i) =>
expect(headers.at(i).text()).toEqual(headerText.label),
);
});
it('should stack on smaller devices', () => {
expect(findTable().classes()).toContain('b-table-stacked-md');
});
});
});
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 * as types from '~/clusters_list/store/mutation_types';
import * as actions from '~/clusters_list/store/actions';
jest.mock('~/flash.js');
describe('Clusters store actions', () => {
describe('fetchClusters', () => {
let mock;
const endpoint = '/clusters';
const clusters = [{ name: 'test' }];
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => mock.restore());
it('should commit SET_CLUSTERS_DATA with received response', done => {
mock.onGet().reply(200, clusters);
testAction(
actions.fetchClusters,
{ endpoint },
{},
[
{ type: types.SET_CLUSTERS_DATA, payload: clusters },
{ type: types.SET_LOADING_STATE, payload: false },
],
[],
() => done(),
);
});
it('should show flash on API error', done => {
mock.onGet().reply(400, 'Not Found');
testAction(actions.fetchClusters, { endpoint }, {}, [], [], () => {
expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
done();
});
});
});
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
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