Commit 4cd9de58 authored by Zack Cuddy's avatar Zack Cuddy Committed by Filipa Lacerda

Design Rep Sync Status - Initialize

This adds the base ruby/rails files
This adds the DOM hook for Vue
This adds the app.vue and index.js

Init the Vuex Store

This adds all actions/mutations
This also inits the state/constansts

Add api endpoint

Vuex Store Tests

This tests all the store functionality
This also creates MOCK_DATA

Lock behind feature flag

Prettyify and Eslint

Forgot to run linting commands

Remove whitespace

Fix empty line

Fix more lint issues

Add a few fixes

Missed a lint

Fixes based on @filipa review

More feedback changes

Fix the broken service

Delete disabled page

Spec feedback

Round of linting

Manual add to gitlab.pot

Store spec and app spec

Tests

Linting and i18n

Service test

A few more fixes

Copy past issue

Add test in api spec

A round of linting

lint

Weird lint

So many linting commands

Rename api function

Moar linting

Lint
parent 3abdd4f6
......@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
export default {
...Api,
geoNodesPath: '/api/:version/geo_nodes',
geoDesignsPath: '/api/:version/geo_replication/designs',
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription',
childEpicPath: '/api/:version/groups/:id/epics/:epic_iid/epics',
......@@ -204,4 +205,9 @@ export default {
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
.nav-icon-container
= sprite_icon('location-dot')
......@@ -22,6 +22,11 @@
= link_to admin_geo_projects_path, title: 'Projects' do
%span
= _('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
= link_to admin_geo_uploads_path, title: 'Uploads' do
%span
......
......@@ -51,6 +51,8 @@ namespace :admin do
resource :settings, only: [:show, :update]
resources :designs, only: [:index]
resources :uploads, only: [:index, :destroy]
end
......
......@@ -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 ""
msgid "Deselect all"
msgstr ""
msgid "Design Management"
msgstr ""
msgid "Design Management files and data"
msgstr ""
msgid "Design Sync Not Enabled"
msgstr ""
msgid "DesignManagement|%{current_design} of %{designs_count}"
msgstr ""
......@@ -5855,6 +5861,9 @@ msgstr ""
msgid "Designs"
msgstr ""
msgid "Designs coming soon."
msgstr ""
msgid "Destroy"
msgstr ""
......@@ -7810,12 +7819,18 @@ msgstr ""
msgid "Geo"
msgstr ""
msgid "Geo Designs"
msgstr ""
msgid "Geo Nodes"
msgstr ""
msgid "Geo Settings"
msgstr ""
msgid "Geo Troubleshooting"
msgstr ""
msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
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}"
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."
msgstr ""
......@@ -17561,6 +17579,9 @@ msgstr ""
msgid "There was an error fetching label data for the selected group"
msgstr ""
msgid "There was an error fetching the Designs"
msgstr ""
msgid "There was an error gathering the chart data"
msgstr ""
......
......@@ -171,7 +171,10 @@ describe('test errors', () => {
// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
if (process.env.BABEL_ENV === 'coverage') {
// 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() {
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