Commit 29a29bdd authored by Mark Florian's avatar Mark Florian

Merge branch '208470_02-geo-replication-api' into 'master'

Geo Replication - Parameterized API

See merge request gitlab-org/gitlab!26702
parents 2e0e8c8b fcf5cfc2
......@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
export default {
...Api,
geoNodesPath: '/api/:version/geo_nodes',
geoDesignsPath: '/api/:version/geo_replication/designs',
geoReplicationPath: '/api/:version/geo_replication/:replicable',
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription',
childEpicPath: '/api/:version/groups/:id/epics/:epic_iid/epics',
......@@ -215,18 +215,18 @@ export default {
return axios.get(url, { params });
},
getGeoDesigns(params = {}) {
const url = Api.buildUrl(this.geoDesignsPath);
getGeoReplicableItems(replicable, params = {}) {
const url = Api.buildUrl(this.geoReplicationPath).replace(':replicable', replicable);
return axios.get(url, { params });
},
initiateAllGeoDesignSyncs(action) {
const url = Api.buildUrl(this.geoDesignsPath);
initiateAllGeoReplicableSyncs(replicable, action) {
const url = Api.buildUrl(this.geoReplicationPath).replace(':replicable', replicable);
return axios.post(`${url}/${action}`, {});
},
initiateGeoDesignSync({ projectId, action }) {
const url = Api.buildUrl(this.geoDesignsPath);
initiateGeoReplicableSync(replicable, { projectId, action }) {
const url = Api.buildUrl(this.geoReplicationPath).replace(':replicable', replicable);
return axios.put(`${url}/${projectId}/${action}`, {});
},
......
......@@ -26,10 +26,6 @@ export default {
type: String,
required: true,
},
designManagementLink: {
type: String,
required: true,
},
},
computed: {
...mapState(['isLoading', 'totalDesigns']),
......
<script>
import { mapState } from 'vuex';
import { GlEmptyState } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
......@@ -18,6 +19,7 @@ export default {
},
},
computed: {
...mapState(['replicableType']),
linkText() {
return sprintf(
s__(
......@@ -35,7 +37,10 @@ export default {
</script>
<template>
<gl-empty-state :title="__('No Design Repositories match this filter')" :svg-path="issuesSvgPath">
<gl-empty-state
:title="sprintf(__('No %{replicableType} match this filter'), { replicableType })"
:svg-path="issuesSvgPath"
>
<template #description>
<div class="text-center">
<p>{{ __('Adjust your filters/search criteria above.') }}</p>
......
......@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { debounce } from 'underscore';
import { GlTabs, GlTab, GlFormInput, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { DEFAULT_SEARCH_DELAY, ACTION_TYPES } from '../store/constants';
......@@ -16,7 +17,7 @@ export default {
Icon,
},
computed: {
...mapState(['currentFilterIndex', 'filterOptions', 'searchFilter']),
...mapState(['currentFilterIndex', 'filterOptions', 'searchFilter', 'replicableType']),
search: {
get() {
return this.searchFilter;
......@@ -26,6 +27,9 @@ export default {
this.fetchDesigns();
}, DEFAULT_SEARCH_DELAY),
},
resyncText() {
return sprintf(__('Resync all %{replicableType}'), { replicableType: this.replicableType });
},
},
methods: {
...mapActions(['setFilter', 'setSearch', 'fetchDesigns', 'initiateAllDesignSyncs']),
......@@ -57,9 +61,9 @@ export default {
<icon name="chevron-down" />
</span>
</template>
<gl-dropdown-item @click="initiateAllDesignSyncs($options.actionTypes.RESYNC)">{{
__('Resync all designs')
}}</gl-dropdown-item>
<gl-dropdown-item @click="initiateAllDesignSyncs($options.actionTypes.RESYNC)">
{{ resyncText }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</template>
......
......@@ -7,23 +7,23 @@ Vue.use(Translate);
export default () => {
const el = document.getElementById('js-geo-designs');
const { replicableType } = el.dataset;
return new Vue({
el,
store: createStore(),
store: createStore(replicableType),
components: {
GeoDesignsApp,
},
data() {
const {
dataset: { geoSvgPath, issuesSvgPath, geoTroubleshootingLink, designManagementLink },
dataset: { geoSvgPath, issuesSvgPath, geoTroubleshootingLink },
} = this.$options.el;
return {
geoSvgPath,
issuesSvgPath,
geoTroubleshootingLink,
designManagementLink,
};
},
......@@ -33,7 +33,6 @@ export default () => {
geoSvgPath: this.geoSvgPath,
issuesSvgPath: this.issuesSvgPath,
geoTroubleshootingLink: this.geoTroubleshootingLink,
designManagementLink: this.designManagementLink,
},
});
},
......
import Api from 'ee/api';
import createFlash from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import {
parseIntPagination,
normalizeHeaders,
......@@ -14,8 +14,12 @@ import { FILTER_STATES } from './constants';
export const requestReplicableItems = ({ commit }) => commit(types.REQUEST_REPLICABLE_ITEMS);
export const receiveReplicableItemsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_REPLICABLE_ITEMS_SUCCESS, data);
export const receiveReplicableItemsError = ({ commit }) => {
createFlash(__('There was an error fetching the designs'));
export const receiveReplicableItemsError = ({ state, commit }) => {
createFlash(
sprintf(__('There was an error fetching the %{replicableType}'), {
replicableType: state.replicableType,
}),
);
commit(types.RECEIVE_REPLICABLE_ITEMS_ERROR);
};
......@@ -31,7 +35,7 @@ export const fetchDesigns = ({ state, dispatch }) => {
sync_status: statusFilterName === FILTER_STATES.ALL ? null : statusFilterName,
};
Api.getGeoDesigns(query)
Api.getGeoReplicableItems(state.replicableType, query)
.then(res => {
const normalizedHeaders = normalizeHeaders(res.headers);
const paginationInformation = parseIntPagination(normalizedHeaders);
......@@ -51,20 +55,32 @@ export const fetchDesigns = ({ state, dispatch }) => {
// Initiate All Replicable Syncs
export const requestInitiateAllReplicableSyncs = ({ commit }) =>
commit(types.REQUEST_INITIATE_ALL_REPLICABLE_SYNCS);
export const receiveInitiateAllReplicableSyncsSuccess = ({ commit, dispatch }, { action }) => {
toast(__(`All designs are being scheduled for ${action}`));
export const receiveInitiateAllReplicableSyncsSuccess = (
{ state, commit, dispatch },
{ action },
) => {
toast(
sprintf(__('All %{replicableType} are being scheduled for %{action}'), {
replicableType: state.replicableType,
action,
}),
);
commit(types.RECEIVE_INITIATE_ALL_REPLICABLE_SYNCS_SUCCESS);
dispatch('fetchReplicableItems');
dispatch('fetchDesigns');
};
export const receiveInitiateAllReplicableSyncsError = ({ commit }) => {
createFlash(__('There was an error syncing the designs.'));
export const receiveInitiateAllReplicableSyncsError = ({ state, commit }) => {
createFlash(
sprintf(__('There was an error syncing the %{replicableType}'), {
replicableType: state.replicableType,
}),
);
commit(types.RECEIVE_INITIATE_ALL_REPLICABLE_SYNCS_ERROR);
};
export const initiateAllDesignSyncs = ({ dispatch }, action) => {
export const initiateAllDesignSyncs = ({ state, dispatch }, action) => {
dispatch('requestInitiateAllReplicableSyncs');
Api.initiateAllGeoDesignSyncs(action)
Api.initiateAllGeoReplicableSyncs(state.replicableType, action)
.then(() => dispatch('receiveInitiateAllReplicableSyncsSuccess', { action }))
.catch(() => {
dispatch('receiveInitiateAllReplicableSyncsError');
......@@ -75,19 +91,19 @@ export const initiateAllDesignSyncs = ({ dispatch }, action) => {
export const requestInitiateReplicableSync = ({ commit }) =>
commit(types.REQUEST_INITIATE_REPLICABLE_SYNC);
export const receiveInitiateReplicableSyncSuccess = ({ commit, dispatch }, { name, action }) => {
toast(__(`${name} is scheduled for ${action}`));
toast(sprintf(__('%{name} is scheduled for %{action}'), { name, action }));
commit(types.RECEIVE_INITIATE_REPLICABLE_SYNC_SUCCESS);
dispatch('fetchReplicableItems');
dispatch('fetchDesigns');
};
export const receiveInitiateReplicableSyncError = ({ commit }, { name }) => {
createFlash(__(`There was an error syncing project '${name}'`));
createFlash(sprintf(__('There was an error syncing project %{name}'), { name }));
commit(types.RECEIVE_INITIATE_REPLICABLE_SYNC_ERROR);
};
export const initiateDesignSync = ({ dispatch }, { projectId, name, action }) => {
export const initiateDesignSync = ({ state, dispatch }, { projectId, name, action }) => {
dispatch('requestInitiateReplicableSync');
Api.initiateGeoDesignSync({ projectId, action })
Api.initiateGeoReplicableSync(state.replicableType, { projectId, action })
.then(() => dispatch('receiveInitiateReplicableSyncSuccess', { name, action }))
.catch(() => {
dispatch('receiveInitiateReplicableSyncError', { name });
......
......@@ -6,10 +6,10 @@ import createState from './state';
Vue.use(Vuex);
const createStore = () =>
const createStore = replicableType =>
new Vuex.Store({
actions,
mutations,
state: createState(),
state: createState(replicableType),
});
export default createStore;
import { FILTER_STATES } from './constants';
const createState = () => ({
const createState = replicableType => ({
replicableType,
isLoading: false,
designs: [],
......
......@@ -3,4 +3,4 @@
#js-geo-designs{ data: { "geo-svg-path" => image_path('illustrations/empty-state/geo-empty.svg'),
"issues-svg-path" => image_path('illustrations/issues.svg'),
"geo-troubleshooting-link" => help_page_path('administration/geo/replication/troubleshooting.html'),
"design-management-link" => help_page_path('user/project/issues/design_management.html') } }
"replicable-type" => 'designs' } }
......@@ -550,17 +550,19 @@ describe('Api', () => {
});
});
describe('GeoDesigns', () => {
describe('GeoReplicable', () => {
let expectedUrl;
let apiResponse;
let mockParams;
let mockReplicableType;
beforeEach(() => {
expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/geo_replication/designs`;
mockReplicableType = 'designs';
expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/geo_replication/${mockReplicableType}`;
});
describe('getGeoDesigns', () => {
it('fetches designs', () => {
describe('getGeoReplicableItems', () => {
it('fetches replicableItems based on replicableType', () => {
apiResponse = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }];
mockParams = { page: 1 };
......@@ -568,14 +570,14 @@ describe('Api', () => {
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(200, apiResponse);
return Api.getGeoDesigns(mockParams).then(({ data }) => {
return Api.getGeoReplicableItems(mockReplicableType, mockParams).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.get).toHaveBeenCalledWith(expectedUrl, { params: mockParams });
});
});
});
describe('initiateAllGeoDesignSyncs', () => {
describe('initiateAllGeoReplicableSyncs', () => {
it('POSTs with correct action', () => {
apiResponse = [{ status: 'ok' }];
mockParams = {};
......@@ -586,14 +588,16 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
mock.onPost(`${expectedUrl}/${mockAction}`).replyOnce(201, apiResponse);
return Api.initiateAllGeoDesignSyncs(mockAction).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.post).toHaveBeenCalledWith(`${expectedUrl}/${mockAction}`, mockParams);
});
return Api.initiateAllGeoReplicableSyncs(mockReplicableType, mockAction).then(
({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.post).toHaveBeenCalledWith(`${expectedUrl}/${mockAction}`, mockParams);
},
);
});
});
describe('initiateGeoDesignSync', () => {
describe('initiateGeoReplicableSync', () => {
it('PUTs with correct action and projectId', () => {
apiResponse = [{ status: 'ok' }];
mockParams = {};
......@@ -605,15 +609,16 @@ describe('Api', () => {
jest.spyOn(axios, 'put');
mock.onPut(`${expectedUrl}/${mockProjectId}/${mockAction}`).replyOnce(201, apiResponse);
return Api.initiateGeoDesignSync({ projectId: mockProjectId, action: mockAction }).then(
({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.put).toHaveBeenCalledWith(
`${expectedUrl}/${mockProjectId}/${mockAction}`,
mockParams,
);
},
);
return Api.initiateGeoReplicableSync(mockReplicableType, {
projectId: mockProjectId,
action: mockAction,
}).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.put).toHaveBeenCalledWith(
`${expectedUrl}/${mockProjectId}/${mockAction}`,
mockParams,
);
});
});
});
});
......
......@@ -10,7 +10,6 @@ import {
MOCK_GEO_SVG_PATH,
MOCK_ISSUES_SVG_PATH,
MOCK_GEO_TROUBLESHOOTING_LINK,
MOCK_DESIGN_MANAGEMENT_LINK,
MOCK_BASIC_FETCH_DATA_MAP,
} from '../mock_data';
......@@ -24,7 +23,6 @@ describe('GeoDesignsApp', () => {
geoSvgPath: MOCK_GEO_SVG_PATH,
issuesSvgPath: MOCK_ISSUES_SVG_PATH,
geoTroubleshootingLink: MOCK_GEO_TROUBLESHOOTING_LINK,
designManagementLink: MOCK_DESIGN_MANAGEMENT_LINK,
};
const actionSpies = {
......
......@@ -7,8 +7,7 @@ export const MOCK_ISSUES_SVG_PATH = 'illustrations/issues.svg';
export const MOCK_GEO_TROUBLESHOOTING_LINK =
'https://docs.gitlab.com/ee/administration/geo/replication/troubleshooting.html';
export const MOCK_DESIGN_MANAGEMENT_LINK =
'https://docs.gitlab.com/ee/user/project/issues/design_management.html';
export const MOCK_REPLICABLE_TYPE = 'designs';
export const MOCK_BASIC_FETCH_RESPONSE = {
data: [
......
......@@ -11,6 +11,7 @@ import {
MOCK_BASIC_FETCH_DATA_MAP,
MOCK_BASIC_FETCH_RESPONSE,
MOCK_BASIC_POST_RESPONSE,
MOCK_REPLICABLE_TYPE,
} from '../mock_data';
jest.mock('~/flash');
......@@ -21,7 +22,7 @@ describe('GeoDesigns Store Actions', () => {
let mock;
beforeEach(() => {
state = createState();
state = createState(MOCK_REPLICABLE_TYPE);
mock = new MockAdapter(axios);
});
......@@ -79,7 +80,13 @@ describe('GeoDesigns Store Actions', () => {
.replyOnce(200, MOCK_BASIC_FETCH_RESPONSE.data, MOCK_BASIC_FETCH_RESPONSE.headers);
});
it('should dispatch the request and success actions', done => {
it('should dispatch the request with correct replicable param and success actions', () => {
function fetchReplicableItemsCall() {
const callHistory = mock.history.get[0];
expect(callHistory.url).toContain(`/geo_replication/${MOCK_REPLICABLE_TYPE}`);
}
testAction(
actions.fetchDesigns,
{},
......@@ -89,7 +96,7 @@ describe('GeoDesigns Store Actions', () => {
{ type: 'requestReplicableItems' },
{ type: 'receiveReplicableItemsSuccess', payload: MOCK_BASIC_FETCH_DATA_MAP },
],
done,
fetchReplicableItemsCall,
);
});
});
......@@ -120,12 +127,13 @@ describe('GeoDesigns Store Actions', () => {
});
describe('no params set', () => {
it('should call fetchDesigns with default queryParams', () => {
it('should call fetchDesigns with default queryParams and correct replicable params', () => {
state.isLoading = true;
function fetchDesignsCall() {
function fetchReplicableItemsCall() {
const callHistory = mock.history.get[0];
expect(callHistory.url).toContain(`/geo_replication/${MOCK_REPLICABLE_TYPE}`);
expect(callHistory.params.page).toEqual(1);
expect(callHistory.params.search).toBeNull();
expect(callHistory.params.sync_status).toBeNull();
......@@ -140,7 +148,7 @@ describe('GeoDesigns Store Actions', () => {
{ type: 'requestReplicableItems' },
{ type: 'receiveReplicableItemsSuccess', payload: MOCK_BASIC_FETCH_DATA_MAP },
],
fetchDesignsCall,
fetchReplicableItemsCall,
);
});
});
......@@ -152,9 +160,10 @@ describe('GeoDesigns Store Actions', () => {
state.searchFilter = 'test search';
state.currentFilterIndex = 2;
function fetchDesignsCall() {
function fetchReplicableItemsCall() {
const callHistory = mock.history.get[0];
expect(callHistory.url).toContain(`/geo_replication/${MOCK_REPLICABLE_TYPE}`);
expect(callHistory.params.page).toEqual(state.currentPage);
expect(callHistory.params.search).toEqual(state.searchFilter);
expect(callHistory.params.sync_status).toEqual(
......@@ -171,7 +180,7 @@ describe('GeoDesigns Store Actions', () => {
{ type: 'requestReplicableItems' },
{ type: 'receiveReplicableItemsSuccess', payload: MOCK_BASIC_FETCH_DATA_MAP },
],
fetchDesignsCall,
fetchReplicableItemsCall,
);
});
});
......@@ -191,13 +200,13 @@ describe('GeoDesigns Store Actions', () => {
});
describe('receiveInitiateAllReplicableSyncsSuccess', () => {
it('should commit mutation RECEIVE_INITIATE_ALL_REPLICABLE_SYNCS_SUCCESS and call fetchReplicableItems and toast', () => {
it('should commit mutation RECEIVE_INITIATE_ALL_REPLICABLE_SYNCS_SUCCESS and call fetchDesigns and toast', () => {
testAction(
actions.receiveInitiateAllReplicableSyncsSuccess,
{ action: ACTION_TYPES.RESYNC },
state,
[{ type: types.RECEIVE_INITIATE_ALL_REPLICABLE_SYNCS_SUCCESS }],
[{ type: 'fetchReplicableItems' }],
[{ type: 'fetchDesigns' }],
() => {
expect(toast).toHaveBeenCalledTimes(1);
toast.mockClear();
......@@ -232,7 +241,13 @@ describe('GeoDesigns Store Actions', () => {
mock.onPost().replyOnce(201, MOCK_BASIC_POST_RESPONSE);
});
it('should dispatch the request and success actions', done => {
it('should dispatch the request with correct replicable param and success actions', () => {
function fetchReplicableItemsCall() {
const callHistory = mock.history.post[0];
expect(callHistory.url).toContain(`/geo_replication/${MOCK_REPLICABLE_TYPE}`);
}
testAction(
actions.initiateAllDesignSyncs,
action,
......@@ -242,7 +257,7 @@ describe('GeoDesigns Store Actions', () => {
{ type: 'requestInitiateAllReplicableSyncs' },
{ type: 'receiveInitiateAllReplicableSyncsSuccess', payload: { action } },
],
done,
fetchReplicableItemsCall,
);
});
});
......@@ -284,13 +299,13 @@ describe('GeoDesigns Store Actions', () => {
});
describe('receiveInitiateReplicableSyncSuccess', () => {
it('should commit mutation RECEIVE_INITIATE_REPLICABLE_SYNC_SUCCESS and call fetchReplicableItems and toast', () => {
it('should commit mutation RECEIVE_INITIATE_REPLICABLE_SYNC_SUCCESS and call fetchDesigns and toast', () => {
testAction(
actions.receiveInitiateReplicableSyncSuccess,
{ action: ACTION_TYPES.RESYNC, projectName: 'test' },
state,
[{ type: types.RECEIVE_INITIATE_REPLICABLE_SYNC_SUCCESS }],
[{ type: 'fetchReplicableItems' }],
[{ type: 'fetchDesigns' }],
() => {
expect(toast).toHaveBeenCalledTimes(1);
toast.mockClear();
......@@ -329,7 +344,13 @@ describe('GeoDesigns Store Actions', () => {
mock.onPut().replyOnce(201, MOCK_BASIC_POST_RESPONSE);
});
it('should dispatch the request and success actions', done => {
it('should dispatch the request with correct replicable param and success actions', () => {
function fetchReplicableItemsCall() {
const callHistory = mock.history.put[0];
expect(callHistory.url).toContain(`/geo_replication/${MOCK_REPLICABLE_TYPE}`);
}
testAction(
actions.initiateDesignSync,
{ projectId, name, action },
......@@ -339,7 +360,7 @@ describe('GeoDesigns Store Actions', () => {
{ type: 'requestInitiateReplicableSync' },
{ type: 'receiveInitiateReplicableSyncSuccess', payload: { name, action } },
],
done,
fetchReplicableItemsCall,
);
});
});
......
......@@ -363,6 +363,9 @@ msgstr ""
msgid "%{name} found %{resultsString}"
msgstr ""
msgid "%{name} is scheduled for %{action}"
msgstr ""
msgid "%{name}'s avatar"
msgstr ""
......@@ -1585,6 +1588,9 @@ msgstr ""
msgid "All"
msgstr ""
msgid "All %{replicableType} are being scheduled for %{action}"
msgstr ""
msgid "All Members"
msgstr ""
......@@ -13145,7 +13151,7 @@ msgstr ""
msgid "No %{providerTitle} repositories found"
msgstr ""
msgid "No Design Repositories match this filter"
msgid "No %{replicableType} match this filter"
msgstr ""
msgid "No Epic"
......@@ -16902,7 +16908,7 @@ msgstr ""
msgid "Resync"
msgstr ""
msgid "Resync all designs"
msgid "Resync all %{replicableType}"
msgstr ""
msgid "Retry"
......@@ -20019,10 +20025,10 @@ msgstr ""
msgid "There was an error fetching median data for stages"
msgstr ""
msgid "There was an error fetching the Node's Groups"
msgid "There was an error fetching the %{replicableType}"
msgstr ""
msgid "There was an error fetching the designs"
msgid "There was an error fetching the Node's Groups"
msgstr ""
msgid "There was an error fetching the environments information."
......@@ -20067,7 +20073,10 @@ msgstr ""
msgid "There was an error subscribing to this label."
msgstr ""
msgid "There was an error syncing the designs."
msgid "There was an error syncing project %{name}"
msgstr ""
msgid "There was an error syncing the %{replicableType}"
msgstr ""
msgid "There was an error trying to validate your query"
......
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