Commit 8cf6847b authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'design-repo-init' into 'master'

Design Repo Sync Status - Initialize

See merge request gitlab-org/gitlab!19975
parents 3abdd4f6 4cd9de58
...@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
export default { export default {
...Api, ...Api,
geoNodesPath: '/api/:version/geo_nodes', geoNodesPath: '/api/:version/geo_nodes',
geoDesignsPath: '/api/:version/geo_replication/designs',
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json', ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription', subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription',
childEpicPath: '/api/:version/groups/:id/epics/:epic_iid/epics', childEpicPath: '/api/:version/groups/:id/epics/:epic_iid/epics',
...@@ -204,4 +205,9 @@ export default { ...@@ -204,4 +205,9 @@ export default {
params, params,
}); });
}, },
getGeoDesigns(params = {}) {
const url = Api.buildUrl(this.geoDesignsPath);
return axios.get(url, { params });
},
}; };
<script>
import GeoDesignsDisabled from './geo_designs_disabled.vue';
export default {
name: 'GeoDesignsApp',
components: {
GeoDesignsDisabled,
},
props: {
geoSvgPath: {
type: String,
required: true,
},
geoTroubleshootingLink: {
type: String,
required: true,
},
designManagementLink: {
type: String,
required: true,
},
designsEnabled: {
type: Boolean,
required: true,
},
},
};
</script>
<template>
<article class="geo-designs-container">
<h2 v-if="designsEnabled">{{ __('Designs coming soon.') }}</h2>
<geo-designs-disabled
v-else
:geo-svg-path="geoSvgPath"
:geo-troubleshooting-link="geoTroubleshootingLink"
:design-management-link="designManagementLink"
/>
</article>
</template>
<script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
export default {
name: 'GeoDesignsDisabled',
components: {
GlEmptyState,
GlButton,
},
props: {
geoSvgPath: {
type: String,
required: true,
},
geoTroubleshootingLink: {
type: String,
required: true,
},
designManagementLink: {
type: String,
required: true,
},
},
};
</script>
<template>
<gl-empty-state :title="__('Design Sync Not Enabled')" :svg-path="geoSvgPath">
<template v-slot:description>
<div class="text-center">
<p>
{{
__(
'If you believe this page to be an error, check out the links below for more information.',
)
}}
</p>
<div>
<gl-button :href="geoTroubleshootingLink" new-style>{{
__('Geo Troubleshooting')
}}</gl-button>
<gl-button :href="designManagementLink" new-style>{{
__('Design Management')
}}</gl-button>
</div>
</div>
</template>
</gl-empty-state>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import createStore from './store';
import GeoDesignsApp from './components/app.vue';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-geo-designs');
return new Vue({
el,
store: createStore(),
components: {
GeoDesignsApp,
},
data() {
const {
dataset: { geoSvgPath, geoTroubleshootingLink, designManagementLink, designsEnabled },
} = this.$options.el;
return {
geoSvgPath,
geoTroubleshootingLink,
designManagementLink,
designsEnabled,
};
},
render(createElement) {
return createElement('geo-designs-app', {
props: {
geoSvgPath: this.geoSvgPath,
geoTroubleshootingLink: this.geoTroubleshootingLink,
designManagementLink: this.designManagementLink,
designsEnabled: this.designsEnabled,
},
});
},
});
};
import createFlash from '~/flash';
import { __ } from '~/locale';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import Api from 'ee/api';
import * as types from './mutation_types';
// Fetch Designs
export const requestDesigns = ({ commit }) => commit(types.REQUEST_DESIGNS);
export const receiveDesignsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_DESIGNS_SUCCESS, data);
export const receiveDesignsError = ({ commit }) => {
createFlash(__('There was an error fetching the Designs'));
commit(types.RECEIVE_DESIGNS_ERROR);
};
export const fetchDesigns = ({ state, dispatch }) => {
dispatch('requestDesigns');
const { currentPage: page } = state;
const query = { page };
Api.getGeoDesigns(query)
.then(res => {
const normalizedHeaders = normalizeHeaders(res.headers);
const paginationInformation = parseIntPagination(normalizedHeaders);
dispatch('receiveDesignsSuccess', {
data: res.data,
perPage: paginationInformation.perPage,
total: paginationInformation.total,
});
})
.catch(() => {
dispatch('receiveDesignsError');
});
};
// Pagination
export const setPage = ({ commit }, page) => {
commit(types.SET_PAGE, page);
};
// eslint-disable-next-line import/prefer-default-export
export const FILTER_STATES = {
ALL: 'all',
SYNCED: 'synced',
PENDING: 'pending',
FAILED: 'failed',
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
actions,
mutations,
state: createState(),
});
export default createStore;
export const SET_PAGE = 'SET_PAGE';
export const REQUEST_DESIGNS = 'REQUEST_DESIGNS';
export const RECEIVE_DESIGNS_SUCCESS = 'RECEIVE_DESIGNS_SUCCESS';
export const RECEIVE_DESIGNS_ERROR = 'RECEIVE_DESIGNS_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_PAGE](state, page) {
state.currentPage = page;
},
[types.REQUEST_DESIGNS](state) {
state.isLoading = true;
},
[types.RECEIVE_DESIGNS_SUCCESS](state, { data, perPage, total }) {
state.isLoading = false;
state.designs = data;
state.pageSize = perPage;
state.totalDesigns = total;
},
[types.RECEIVE_DESIGNS_ERROR](state) {
state.isLoading = false;
state.designs = [];
state.pageSize = 0;
state.totalDesigns = 0;
},
};
const createState = () => ({
isLoading: false,
designs: [],
totalDesigns: 0,
pageSize: 0,
currentPage: 1,
});
export default createState;
import initGeoDesigns from 'ee/geo_designs';
document.addEventListener('DOMContentLoaded', initGeoDesigns);
# frozen_string_literal: true
class Admin::Geo::DesignsController < Admin::Geo::ApplicationController
before_action :check_license!
def index
end
end
- page_title _('Geo Designs')
- @content_class = "geo-admin-container"
#js-geo-designs{ data: { "geo-svg-path" => image_path('illustrations/gitlab_geo.svg'),
"geo-troubleshooting-link" => help_page_path('administration/geo/replication/troubleshooting.html'),
"design-management-link" => help_page_path('user/project/issues/design_management.html'),
"designs-enabled" => Feature.enabled?(:enable_geo_design_sync) && Feature.enabled?(:enable_geo_design_view) } }
= nav_link(controller: %w(admin/geo/nodes admin/geo/projects admin/geo/uploads admin/geo/settings)) do = nav_link(controller: %w(admin/geo/nodes admin/geo/projects admin/geo/uploads admin/geo/settings admin/geo/uploads)) do
= link_to admin_geo_nodes_path, class: "qa-link-geo-menu" do = link_to admin_geo_nodes_path, class: "qa-link-geo-menu" do
.nav-icon-container .nav-icon-container
= sprite_icon('location-dot') = sprite_icon('location-dot')
...@@ -22,6 +22,11 @@ ...@@ -22,6 +22,11 @@
= link_to admin_geo_projects_path, title: 'Projects' do = link_to admin_geo_projects_path, title: 'Projects' do
%span %span
= _('Projects') = _('Projects')
- if Feature.enabled?(:enable_geo_design_sync) && Feature.enabled?(:enable_geo_design_view)
= nav_link(path: 'admin/geo/designs#index') do
= link_to admin_geo_designs_path, title: _('Designs') do
%span
= _('Designs')
= nav_link(path: 'admin/geo/uploads#index') do = nav_link(path: 'admin/geo/uploads#index') do
= link_to admin_geo_uploads_path, title: 'Uploads' do = link_to admin_geo_uploads_path, title: 'Uploads' do
%span %span
......
...@@ -51,6 +51,8 @@ namespace :admin do ...@@ -51,6 +51,8 @@ namespace :admin do
resource :settings, only: [:show, :update] resource :settings, only: [:show, :update]
resources :designs, only: [:index]
resources :uploads, only: [:index, :destroy] resources :uploads, only: [:index, :destroy]
end end
......
...@@ -502,4 +502,21 @@ describe('Api', () => { ...@@ -502,4 +502,21 @@ describe('Api', () => {
}); });
}); });
}); });
describe('getGeoDesigns', () => {
it('fetches designs', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/geo_replication/designs`;
const apiResponse = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }];
const mockParams = { page: 1 };
jest.spyOn(Api, 'buildUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(200, apiResponse);
return Api.getGeoDesigns(mockParams).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.get).toHaveBeenCalledWith(expectedUrl, { params: mockParams });
});
});
});
}); });
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import GeoDesignsApp from 'ee/geo_designs/components/app.vue';
import store from 'ee/geo_designs/store';
import GeoDesignsDisabled from 'ee/geo_designs/components/geo_designs_disabled.vue';
import {
MOCK_GEO_SVG_PATH,
MOCK_GEO_TROUBLESHOOTING_LINK,
MOCK_DESIGN_MANAGEMENT_LINK,
} from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GeoDesignsApp', () => {
let wrapper;
const propsData = {
geoSvgPath: MOCK_GEO_SVG_PATH,
geoTroubleshootingLink: MOCK_GEO_TROUBLESHOOTING_LINK,
designManagementLink: MOCK_DESIGN_MANAGEMENT_LINK,
designsEnabled: true,
};
const createComponent = () => {
wrapper = shallowMount(localVue.extend(GeoDesignsApp), {
localVue,
store,
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
const findGeoDesignsContainer = () => wrapper.find('.geo-designs-container');
const findDesignsComingSoon = () => findGeoDesignsContainer().find('h2');
const findGeoDesignsDisabled = () => findGeoDesignsContainer().find(GeoDesignsDisabled);
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders the design container', () => {
expect(findGeoDesignsContainer().exists()).toBe(true);
});
describe('when designsEnabled = false', () => {
beforeEach(() => {
propsData.designsEnabled = false;
createComponent();
});
it('hides designs coming soon text', () => {
expect(findDesignsComingSoon().exists()).toBe(false);
});
it('shows designs disabled component', () => {
expect(findGeoDesignsDisabled().exists()).toBe(true);
});
});
describe('when designsEnabled = true', () => {
beforeEach(() => {
propsData.designsEnabled = true;
createComponent();
});
it('shows designs coming soon text', () => {
expect(findDesignsComingSoon().exists()).toBe(true);
});
it('hides designs disabled component', () => {
expect(findGeoDesignsDisabled().exists()).toBe(false);
});
});
});
});
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import { GlButton, GlEmptyState } from '@gitlab/ui';
import GeoDesignsDisabled from 'ee/geo_designs/components/geo_designs_disabled.vue';
import store from 'ee/geo_designs/store';
import {
MOCK_GEO_SVG_PATH,
MOCK_GEO_TROUBLESHOOTING_LINK,
MOCK_DESIGN_MANAGEMENT_LINK,
} from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GeoDesignsDisabled', () => {
let wrapper;
const propsData = {
geoSvgPath: MOCK_GEO_SVG_PATH,
geoTroubleshootingLink: MOCK_GEO_TROUBLESHOOTING_LINK,
designManagementLink: MOCK_DESIGN_MANAGEMENT_LINK,
};
const createComponent = () => {
wrapper = mount(localVue.extend(GeoDesignsDisabled), {
localVue,
store,
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlEmptyState = () => wrapper.find(GlEmptyState);
const findGlButton = () => findGlEmptyState().findAll(GlButton);
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders GlEmptyState', () => {
expect(findGlEmptyState().exists()).toEqual(true);
});
it('renders 2 GlButtons', () => {
expect(findGlButton().length).toEqual(2);
});
});
});
export const MOCK_GEO_SVG_PATH = 'illustrations/gitlab_geo.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_BASIC_FETCH_RESPONSE = {
data: [
{
id: 1,
project_id: 1,
name: 'zack test 1',
state: 'pending',
last_synced_at: null,
},
{
id: 2,
project_id: 2,
name: 'zack test 2',
state: 'synced',
last_synced_at: null,
},
],
headers: {
'x-per-page': 20,
'x-total': 100,
},
};
export const MOCK_BASIC_FETCH_DATA_MAP = {
data: MOCK_BASIC_FETCH_RESPONSE.data,
perPage: MOCK_BASIC_FETCH_RESPONSE.headers['x-per-page'],
total: MOCK_BASIC_FETCH_RESPONSE.headers['x-total'],
};
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import * as actions from 'ee/geo_designs/store/actions';
import * as types from 'ee/geo_designs/store/mutation_types';
import createState from 'ee/geo_designs/store/state';
import { MOCK_BASIC_FETCH_DATA_MAP, MOCK_BASIC_FETCH_RESPONSE } from '../mock_data';
jest.mock('~/flash');
describe('GeoDesigns Store Actions', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('requestDesigns', () => {
it('should commit mutation REQUEST_DESIGNS', done => {
testAction(actions.requestDesigns, null, state, [{ type: types.REQUEST_DESIGNS }], [], done);
});
});
describe('receiveDesignsSuccess', () => {
it('should commit mutation RECEIVE_DESIGNS_SUCCESS', done => {
testAction(
actions.receiveDesignsSuccess,
MOCK_BASIC_FETCH_DATA_MAP,
state,
[{ type: types.RECEIVE_DESIGNS_SUCCESS, payload: MOCK_BASIC_FETCH_DATA_MAP }],
[],
done,
);
});
});
describe('receiveDesignsError', () => {
it('should commit mutation RECEIVE_DESIGNS_ERROR and call flash', done => {
testAction(
actions.receiveDesignsError,
null,
state,
[{ type: types.RECEIVE_DESIGNS_ERROR }],
[],
done,
);
expect(flash).toHaveBeenCalledTimes(1);
});
});
describe('fetchDesigns', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock
.onGet()
.replyOnce(200, MOCK_BASIC_FETCH_RESPONSE.data, MOCK_BASIC_FETCH_RESPONSE.headers);
});
it('should dispatch the request and success actions', done => {
testAction(
actions.fetchDesigns,
{},
state,
[],
[
{ type: 'requestDesigns' },
{ type: 'receiveDesignsSuccess', payload: MOCK_BASIC_FETCH_DATA_MAP },
],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onGet().replyOnce(500, {});
});
it('should dispatch the request and error actions', done => {
testAction(
actions.fetchDesigns,
{},
state,
[],
[{ type: 'requestDesigns' }, { type: 'receiveDesignsError' }],
done,
);
});
});
});
describe('setPage', () => {
it('should commit mutation SET_PAGE', done => {
testAction(actions.setPage, 2, state, [{ type: types.SET_PAGE, payload: 2 }], [], done);
});
});
});
import mutations from 'ee/geo_designs/store/mutations';
import createState from 'ee/geo_designs/store/state';
import * as types from 'ee/geo_designs/store/mutation_types';
import { MOCK_BASIC_FETCH_DATA_MAP } from '../mock_data';
describe('GeoDesigns Store Mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('SET_PAGE', () => {
it('sets the page to the correct page', () => {
mutations[types.SET_PAGE](state, 2);
expect(state.currentPage).toEqual(2);
});
});
describe('REQUEST_DESIGNS', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_DESIGNS](state);
expect(state.isLoading).toEqual(true);
});
});
describe('RECEIVE_DESIGNS_SUCCESS', () => {
let mockData = {};
beforeEach(() => {
mockData = MOCK_BASIC_FETCH_DATA_MAP;
});
it('sets isLoading to false', () => {
state.isLoading = true;
mutations[types.RECEIVE_DESIGNS_SUCCESS](state, mockData);
expect(state.isLoading).toEqual(false);
});
it('sets designs array with design data', () => {
mutations[types.RECEIVE_DESIGNS_SUCCESS](state, mockData);
expect(state.designs).toBe(mockData.data);
});
it('sets pageSize and totalDesigns', () => {
mutations[types.RECEIVE_DESIGNS_SUCCESS](state, mockData);
expect(state.pageSize).toEqual(mockData.perPage);
expect(state.totalDesigns).toEqual(mockData.total);
});
});
describe('RECEIVE_DESIGNS_ERROR', () => {
let mockData = {};
beforeEach(() => {
mockData = MOCK_BASIC_FETCH_DATA_MAP;
});
it('sets isLoading to false', () => {
state.isLoading = true;
mutations[types.RECEIVE_DESIGNS_ERROR](state);
expect(state.isLoading).toEqual(false);
});
it('resets designs array', () => {
state.designs = mockData.data;
mutations[types.RECEIVE_DESIGNS_ERROR](state);
expect(state.designs).toEqual([]);
});
it('resets pagination data', () => {
state.pageSize = mockData.perPage;
state.totalDesigns = mockData.total;
mutations[types.RECEIVE_DESIGNS_ERROR](state);
expect(state.pageSize).toEqual(0);
expect(state.totalDesigns).toEqual(0);
});
});
});
...@@ -5783,9 +5783,15 @@ msgstr "" ...@@ -5783,9 +5783,15 @@ msgstr ""
msgid "Deselect all" msgid "Deselect all"
msgstr "" msgstr ""
msgid "Design Management"
msgstr ""
msgid "Design Management files and data" msgid "Design Management files and data"
msgstr "" msgstr ""
msgid "Design Sync Not Enabled"
msgstr ""
msgid "DesignManagement|%{current_design} of %{designs_count}" msgid "DesignManagement|%{current_design} of %{designs_count}"
msgstr "" msgstr ""
...@@ -5855,6 +5861,9 @@ msgstr "" ...@@ -5855,6 +5861,9 @@ msgstr ""
msgid "Designs" msgid "Designs"
msgstr "" msgstr ""
msgid "Designs coming soon."
msgstr ""
msgid "Destroy" msgid "Destroy"
msgstr "" msgstr ""
...@@ -7810,12 +7819,18 @@ msgstr "" ...@@ -7810,12 +7819,18 @@ msgstr ""
msgid "Geo" msgid "Geo"
msgstr "" msgstr ""
msgid "Geo Designs"
msgstr ""
msgid "Geo Nodes" msgid "Geo Nodes"
msgstr "" msgstr ""
msgid "Geo Settings" msgid "Geo Settings"
msgstr "" msgstr ""
msgid "Geo Troubleshooting"
msgstr ""
msgid "Geo allows you to replicate your GitLab instance to other geographical locations." msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
msgstr "" msgstr ""
...@@ -9228,6 +9243,9 @@ msgstr "" ...@@ -9228,6 +9243,9 @@ msgstr ""
msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}" msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
msgstr "" msgstr ""
msgid "If you believe this page to be an error, check out the links below for more information."
msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes." msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr "" msgstr ""
...@@ -17561,6 +17579,9 @@ msgstr "" ...@@ -17561,6 +17579,9 @@ msgstr ""
msgid "There was an error fetching label data for the selected group" msgid "There was an error fetching label data for the selected group"
msgstr "" msgstr ""
msgid "There was an error fetching the Designs"
msgstr ""
msgid "There was an error gathering the chart data" msgid "There was an error gathering the chart data"
msgstr "" msgstr ""
......
...@@ -171,7 +171,10 @@ describe('test errors', () => { ...@@ -171,7 +171,10 @@ describe('test errors', () => {
// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15 // see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
if (process.env.BABEL_ENV === 'coverage') { if (process.env.BABEL_ENV === 'coverage') {
// exempt these files from the coverage report // exempt these files from the coverage report
const troubleMakers = ['./pages/admin/application_settings/general/index.js']; const troubleMakers = [
'./pages/admin/application_settings/general/index.js',
'./geo_designs/index.js',
];
describe('Uncovered files', function() { describe('Uncovered files', function() {
const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)]; const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
......
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