Commit fcf5cfc2 authored by Zack Cuddy's avatar Zack Cuddy

Geo Replication - Paramaterized API

This MR is an effort at MVC.

This MR splits off from:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26410

The overall focus of this and following related
changes is to replace Geo_dDesigns with
Geo_Replicable.

This MR adds the prop "replicable_type".
This prop is used in the geo_replication api
to denote which type of replicable we are using.

It also removes an un-used prop:
"design_management_link" that had no
usage in the components.

Following MRs:
Public Store Functions and State
Vue Components and Filenames
parent 1438907f
......@@ -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 ""
......@@ -13127,7 +13133,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"
......@@ -16866,7 +16872,7 @@ msgstr ""
msgid "Resync"
msgstr ""
msgid "Resync all designs"
msgid "Resync all %{replicableType}"
msgstr ""
msgid "Retry"
......@@ -19986,10 +19992,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."
......@@ -20034,7 +20040,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