Commit 8f47c9a2 authored by Phil Hughes's avatar Phil Hughes

Merge branch '287945-geo-beta-vuex' into 'master'

Geo Node Status 2.0 - Vuex

See merge request gitlab-org/gitlab!53910
parents 258c8f0b 902b5b83
......@@ -5,6 +5,7 @@ import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
export default {
...Api,
geoNodesPath: '/api/:version/geo_nodes',
geoNodesStatusPath: '/api/:version/geo_nodes/status',
geoReplicationPath: '/api/:version/geo_replication/:replicable',
ldapGroupsPath: '/api/:version/ldap/:provider/groups.json',
subscriptionPath: '/api/:version/namespaces/:id/gitlab_subscription',
......@@ -325,6 +326,16 @@ export default {
return axios.post(url);
},
getGeoNodes() {
const url = Api.buildUrl(this.geoNodesPath);
return axios.get(url);
},
getGeoNodesStatus() {
const url = Api.buildUrl(this.geoNodesStatusPath);
return axios.get(url);
},
createGeoNode(node) {
const url = Api.buildUrl(this.geoNodesPath);
return axios.post(url, node);
......
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Translate from '~/vue_shared/translate';
import GeoNodesBetaApp from './components/app.vue';
import createStore from './store';
Vue.use(Translate);
......@@ -11,8 +13,14 @@ export const initGeoNodesBeta = () => {
return false;
}
const { primaryVersion, primaryRevision } = el.dataset;
let { replicableTypes } = el.dataset;
replicableTypes = convertObjectPropsToCamelCase(JSON.parse(replicableTypes), { deep: true });
return new Vue({
el,
store: createStore({ primaryVersion, primaryRevision, replicableTypes }),
render(createElement) {
return createElement(GeoNodesBetaApp);
},
......
import Api from 'ee/api';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const fetchNodes = ({ commit }) => {
commit(types.REQUEST_NODES);
const promises = [Api.getGeoNodes(), Api.getGeoNodesStatus()];
Promise.all(promises)
.then(([{ data: nodes }, { data: statuses }]) => {
const inflatedNodes = nodes.map((node) =>
convertObjectPropsToCamelCase({
...node,
...statuses.find((status) => status.geo_node_id === node.id),
}),
);
commit(types.RECEIVE_NODES_SUCCESS, inflatedNodes);
})
.catch(() => {
createFlash({ message: __('There was an error fetching the Geo Nodes') });
commit(types.RECEIVE_NODES_ERROR);
});
};
import { isNil } from 'lodash';
import { convertToCamelCase } from '~/lib/utils/text_utility';
export const verificationInfo = (state) => (id) => {
const node = state.nodes.find((n) => n.id === id);
const variables = {};
if (node.primary) {
variables.total = 'ChecksumTotalCount';
variables.success = 'ChecksummedCount';
variables.failed = 'ChecksumFailedCount';
} else {
variables.total = 'VerificationTotalCount';
variables.success = 'VerifiedCount';
variables.failed = 'VerificationFailedCount';
}
return state.replicableTypes
.map((replicable) => {
const camelCaseName = convertToCamelCase(replicable.namePlural);
return {
dataType: replicable.dataType,
dataTypeTitle: replicable.dataTypeTitle,
title: replicable.titlePlural,
values: {
total: node[`${camelCaseName}${variables.total}`],
success: node[`${camelCaseName}${variables.success}`],
failed: node[`${camelCaseName}${variables.failed}`],
},
};
})
.filter((replicable) =>
Boolean(!isNil(replicable.values.success) || !isNil(replicable.values.failed)),
);
};
export const syncInfo = (state) => (id) => {
const node = state.nodes.find((n) => n.id === id);
return state.replicableTypes.map((replicable) => {
const camelCaseName = convertToCamelCase(replicable.namePlural);
return {
dataType: replicable.dataType,
dataTypeTitle: replicable.dataTypeTitle,
title: replicable.titlePlural,
values: {
total: node[`${camelCaseName}Count`],
success: node[`${camelCaseName}SyncedCount`],
failed: node[`${camelCaseName}FailedCount`],
},
};
});
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export const getStoreConfig = ({ primaryVersion, primaryRevision, replicableTypes }) => ({
actions,
getters,
mutations,
state: createState({ primaryVersion, primaryRevision, replicableTypes }),
});
const createStore = (config) => new Vuex.Store(getStoreConfig(config));
export default createStore;
export const REQUEST_NODES = 'REQUEST_NODES';
export const RECEIVE_NODES_SUCCESS = 'RECEIVE_NODES_SUCCESS';
export const RECEIVE_NODES_ERROR = 'RECEIVE_NODES_ERROR';
import * as types from './mutation_types';
export default {
[types.REQUEST_NODES](state) {
state.isLoading = true;
},
[types.RECEIVE_NODES_SUCCESS](state, data) {
state.isLoading = false;
state.nodes = data;
},
[types.RECEIVE_NODES_ERROR](state) {
state.isLoading = false;
state.nodes = [];
},
};
const createState = ({ primaryVersion, primaryRevision, replicableTypes }) => ({
primaryVersion,
primaryRevision,
replicableTypes,
nodes: [],
isLoading: false,
});
export default createState;
......@@ -154,6 +154,8 @@ module EE
# Hard Coded Legacy Types, we will want to remove these when they are added to SSF
replicable_types = [
{
data_type: 'repository',
data_type_title: _('Git'),
title: _('Repository'),
title_plural: _('Repositories'),
name: 'repository',
......@@ -161,18 +163,24 @@ module EE
secondary_view: true
},
{
data_type: 'repository',
data_type_title: _('Git'),
title: _('Wiki'),
title_plural: _('Wikis'),
name: 'wiki',
name_plural: 'wikis'
},
{
data_type: 'blob',
data_type_title: _('File'),
title: _('LFS object'),
title_plural: _('LFS objects'),
name: 'lfs_object',
name_plural: 'lfs_objects'
},
{
data_type: 'blob',
data_type_title: _('File'),
title: _('Attachment'),
title_plural: _('Attachments'),
name: 'attachment',
......@@ -180,18 +188,24 @@ module EE
secondary_view: true
},
{
data_type: 'blob',
data_type_title: _('File'),
title: _('Job artifact'),
title_plural: _('Job artifacts'),
name: 'job_artifact',
name_plural: 'job_artifacts'
},
{
data_type: 'blob',
data_type_title: _('File'),
title: _('Container repository'),
title_plural: _('Container repositories'),
name: 'container_repository',
name_plural: 'container_repositories'
},
{
data_type: 'repository',
data_type_title: _('Git'),
title: _('Design repository'),
title_plural: _('Design repositories'),
name: 'design_repository',
......@@ -204,6 +218,8 @@ module EE
enabled_replicator_classes.each do |replicator_class|
replicable_types.push(
{
data_type: 'blob',
data_type_title: _('File'),
title: replicator_class.replicable_title,
title_plural: replicator_class.replicable_title_plural,
name: replicator_class.replicable_name,
......
- page_title _('Geo Nodes Beta')
- page_title _('Geo sites')
#js-geo-nodes-beta{ }
#js-geo-nodes-beta{ data: node_vue_list_properties }
export const MOCK_PRIMARY_VERSION = {
version: '10.4.0-pre',
revision: 'b93c51849b',
};
export const MOCK_REPLICABLE_TYPES = [
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Repository',
titlePlural: 'Repositories',
name: 'repository',
namePlural: 'repositories',
secondaryView: true,
},
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Wiki',
titlePlural: 'Wikis',
name: 'wiki',
namePlural: 'wikis',
},
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Design',
titlePlural: 'Designs',
name: 'design',
namePlural: 'designs',
secondaryView: true,
},
{
dataType: 'blob',
dataTypeTitle: 'File',
title: 'Package File',
titlePlural: 'Package Files',
name: 'package_file',
namePlural: 'package_files',
},
];
// This const is very specific, it is a hard coded filtered information from MOCK_NODES
// Be sure if updating you follow the pattern else getters_spec.js will fail.
export const MOCK_PRIMARY_VERIFICATION_INFO = [
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Repositories',
values: {
total: 12,
success: 12,
failed: 0,
},
},
];
// This const is very specific, it is a hard coded filtered information from MOCK_NODES
// Be sure if updating you follow the pattern else getters_spec.js will fail.
export const MOCK_SECONDARY_VERIFICATION_INFO = [
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Repositories',
values: {
total: 12,
success: 0,
failed: 12,
},
},
];
// This const is very specific, it is a hard coded filtered information from MOCK_NODES
// Be sure if updating you follow the pattern else getters_spec.js will fail.
export const MOCK_SECONDARY_SYNC_INFO = [
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Repositories',
values: {
total: 12,
success: 12,
failed: 0,
},
},
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Wikis',
values: {
total: 12,
success: 6,
failed: 6,
},
},
{
dataType: 'repository',
dataTypeTitle: 'Git',
title: 'Designs',
values: {
total: 12,
success: 0,
failed: 0,
},
},
{
dataType: 'blob',
dataTypeTitle: 'File',
title: 'Package Files',
values: {
total: 25,
success: 25,
failed: 0,
},
},
];
// This const is very specific, it is a hard coded camelCase version of MOCK_NODES_RES and MOCK_NODE_STATUSES_RES
// Be sure if updating you follow the pattern else actions_spec.js will fail.
export const MOCK_NODES = [
{
id: 1,
name: 'Test Node 1',
url: 'http://127.0.0.1:3001/',
primary: true,
enabled: true,
current: true,
geoNodeId: 1,
healthStatus: 'Healthy',
repositoriesCount: 12,
repositoriesChecksumTotalCount: 12,
repositoriesChecksummedCount: 12,
repositoriesChecksumFailedCount: 0,
replicationSlotsMaxRetainedWalBytes: 502658737,
replicationSlotsCount: 1,
replicationSlotsUsedCount: 0,
version: '10.4.0-pre',
revision: 'b93c51849b',
},
{
id: 2,
name: 'Test Node 2',
url: 'http://127.0.0.1:3002/',
primary: false,
enabled: true,
current: false,
geoNodeId: 2,
healthStatus: 'Healthy',
repositoriesCount: 12,
repositoriesFailedCount: 0,
repositoriesSyncedCount: 12,
repositoriesVerificationTotalCount: 12,
repositoriesVerifiedCount: 0,
repositoriesVerificationFailedCount: 12,
wikisCount: 12,
wikisFailedCount: 6,
wikisSyncedCount: 6,
designsCount: 12,
designsFailedCount: 0,
designsSyncedCount: 0,
packageFilesCount: 25,
packageFilesSyncedCount: 25,
packageFilesFailedCount: 0,
dbReplicationLagSeconds: 0,
lastEventId: 3,
lastEventTimestamp: 1511255200,
cursorLastEventId: 3,
cursorLastEventTimestamp: 1511255200,
version: '10.4.0-pre',
revision: 'b93c51849b',
storageShardsMatch: true,
},
];
export const MOCK_NODES_RES = [
{
id: 1,
name: 'Test Node 1',
url: 'http://127.0.0.1:3001/',
primary: true,
enabled: true,
current: true,
},
{
id: 2,
name: 'Test Node 2',
url: 'http://127.0.0.1:3002/',
primary: false,
enabled: true,
current: false,
},
];
export const MOCK_NODE_STATUSES_RES = [
{
geo_node_id: 1,
health_status: 'Healthy',
repositories_count: 12,
repositories_checksum_total_count: 12,
repositories_checksummed_count: 12,
repositories_checksum_failed_count: 0,
replication_slots_max_retained_wal_bytes: 502658737,
replication_slots_count: 1,
replication_slots_used_count: 0,
version: '10.4.0-pre',
revision: 'b93c51849b',
},
{
geo_node_id: 2,
health_status: 'Healthy',
repositories_count: 12,
repositories_failed_count: 0,
repositories_synced_count: 12,
repositories_verification_total_count: 12,
repositories_verified_count: 0,
repositories_verification_failed_count: 12,
wikis_count: 12,
wikis_failed_count: 6,
wikis_synced_count: 6,
designs_count: 12,
designs_failed_count: 0,
designs_synced_count: 0,
package_files_count: 25,
package_files_synced_count: 25,
package_files_failed_count: 0,
db_replication_lag_seconds: 0,
last_event_id: 3,
last_event_timestamp: 1511255200,
cursor_last_event_id: 3,
cursor_last_event_timestamp: 1511255200,
version: '10.4.0-pre',
revision: 'b93c51849b',
storage_shards_match: true,
},
];
import MockAdapter from 'axios-mock-adapter';
import * as actions from 'ee/geo_nodes_beta/store/actions';
import * as types from 'ee/geo_nodes_beta/store/mutation_types';
import createState from 'ee/geo_nodes_beta/store/state';
import testAction from 'helpers/vuex_action_helper';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
MOCK_PRIMARY_VERSION,
MOCK_REPLICABLE_TYPES,
MOCK_NODES,
MOCK_NODES_RES,
MOCK_NODE_STATUSES_RES,
} from '../mock_data';
jest.mock('~/flash');
describe('GeoNodesBeta Store Actions', () => {
let mock;
let state;
beforeEach(() => {
state = createState({
primaryVersion: MOCK_PRIMARY_VERSION.version,
primaryRevision: MOCK_PRIMARY_VERSION.revision,
replicableTypes: MOCK_REPLICABLE_TYPES,
});
mock = new MockAdapter(axios);
});
afterEach(() => {
state = null;
mock.restore();
});
describe('fetchNodes', () => {
describe('on success', () => {
beforeEach(() => {
mock.onGet(/api\/(.*)\/geo_nodes/).replyOnce(200, MOCK_NODES_RES);
mock.onGet(/api\/(.*)\/geo_nodes\/status/).replyOnce(200, MOCK_NODE_STATUSES_RES);
});
it('should dispatch the correct mutations', () => {
return testAction({
action: actions.fetchNodes,
payload: null,
state,
expectedMutations: [
{ type: types.REQUEST_NODES },
{ type: types.RECEIVE_NODES_SUCCESS, payload: MOCK_NODES },
],
});
});
});
describe('on error', () => {
beforeEach(() => {
mock.onGet(/api\/(.*)\/geo_nodes/).reply(500);
mock.onGet(/api\/(.*)\/geo_nodes\/status/).reply(500);
});
it('should dispatch the correct mutations', () => {
return testAction({
action: actions.fetchNodes,
payload: null,
state,
expectedMutations: [{ type: types.REQUEST_NODES }, { type: types.RECEIVE_NODES_ERROR }],
}).then(() => {
expect(createFlash).toHaveBeenCalledTimes(1);
createFlash.mockClear();
});
});
});
});
});
import * as getters from 'ee/geo_nodes_beta/store/getters';
import createState from 'ee/geo_nodes_beta/store/state';
import {
MOCK_PRIMARY_VERSION,
MOCK_REPLICABLE_TYPES,
MOCK_NODES,
MOCK_PRIMARY_VERIFICATION_INFO,
MOCK_SECONDARY_VERIFICATION_INFO,
MOCK_SECONDARY_SYNC_INFO,
} from '../mock_data';
describe('GeoNodesBeta Store Getters', () => {
let state;
beforeEach(() => {
state = createState({
primaryVersion: MOCK_PRIMARY_VERSION.version,
primaryRevision: MOCK_PRIMARY_VERSION.revision,
replicableTypes: MOCK_REPLICABLE_TYPES,
});
});
describe('verificationInfo', () => {
beforeEach(() => {
state.nodes = MOCK_NODES;
});
describe('on primary node', () => {
it('returns only replicable types that have checksum data', () => {
expect(getters.verificationInfo(state)(MOCK_NODES[0].id)).toStrictEqual(
MOCK_PRIMARY_VERIFICATION_INFO,
);
});
});
describe('on secondary node', () => {
it('returns only replicable types that have verification data', () => {
expect(getters.verificationInfo(state)(MOCK_NODES[1].id)).toStrictEqual(
MOCK_SECONDARY_VERIFICATION_INFO,
);
});
});
});
describe('syncInfo', () => {
beforeEach(() => {
state.nodes = MOCK_NODES;
});
it('returns the nodes sync information', () => {
expect(getters.syncInfo(state)(MOCK_NODES[1].id)).toStrictEqual(MOCK_SECONDARY_SYNC_INFO);
});
});
});
import * as types from 'ee/geo_nodes_beta/store/mutation_types';
import mutations from 'ee/geo_nodes_beta/store/mutations';
import createState from 'ee/geo_nodes_beta/store/state';
import { MOCK_PRIMARY_VERSION, MOCK_REPLICABLE_TYPES, MOCK_NODES } from '../mock_data';
describe('GeoNodesBeta Store Mutations', () => {
let state;
beforeEach(() => {
state = createState({
primaryVersion: MOCK_PRIMARY_VERSION.version,
primaryRevision: MOCK_PRIMARY_VERSION.revision,
replicableTypes: MOCK_REPLICABLE_TYPES,
});
});
describe('REQUEST_NODES', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_NODES](state);
expect(state.isLoading).toBe(true);
});
});
describe('RECEIVE_NODES_SUCCESS', () => {
beforeEach(() => {
state.isLoading = true;
});
it('sets nodes and ends loading', () => {
mutations[types.RECEIVE_NODES_SUCCESS](state, MOCK_NODES);
expect(state.isLoading).toBe(false);
expect(state.nodes).toEqual(MOCK_NODES);
});
});
describe('RECEIVE_NODES_ERROR', () => {
beforeEach(() => {
state.isLoading = true;
state.nodes = MOCK_NODES;
});
it('resets state', () => {
mutations[types.RECEIVE_NODES_ERROR](state);
expect(state.isLoading).toBe(false);
expect(state.nodes).toEqual([]);
});
});
});
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Admin::Geo::NodesBetaController do
RSpec.describe Admin::Geo::NodesBetaController, :geo do
include AdminModeHelper
let_it_be(:admin) { create(:admin) }
......@@ -27,7 +27,7 @@ RSpec.describe Admin::Geo::NodesBetaController do
get admin_geo_nodes_beta_path
expect(response).to render_template(:index)
expect(response.body).to include('Geo Nodes Beta')
expect(response.body).to include('Geo sites')
end
end
......
......@@ -13234,6 +13234,9 @@ msgstr ""
msgid "Geo nodes are paused using a command run on the node"
msgstr ""
msgid "Geo sites"
msgstr ""
msgid "GeoNodeStatusEvent|%{timeAgoStr} (%{pendingEvents} events)"
msgstr ""
......@@ -13612,6 +13615,9 @@ msgstr ""
msgid "Getting started with releases"
msgstr ""
msgid "Git"
msgstr ""
msgid "Git LFS is not enabled on this GitLab server, contact your admin."
msgstr ""
......@@ -29793,6 +29799,9 @@ msgstr ""
msgid "There was an error fetching the %{replicableType}"
msgstr ""
msgid "There was an error fetching the Geo Nodes"
msgstr ""
msgid "There was an error fetching the Geo Settings"
msgstr ""
......
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