Commit 902b5b83 authored by Zack Cuddy's avatar Zack Cuddy Committed by Phil Hughes

Geo Node Status - Vuex

This change adds the data fetching,
state management, actions,
and getters for the Vuex
side of the Geo Node Status 2.0.

This will power the new UI along
with makeing the data fetching
much faster and more concise.
parent 33e02c42
...@@ -5,6 +5,7 @@ import { ContentTypeMultipartFormData } from '~/lib/utils/headers'; ...@@ -5,6 +5,7 @@ import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
export default { export default {
...Api, ...Api,
geoNodesPath: '/api/:version/geo_nodes', geoNodesPath: '/api/:version/geo_nodes',
geoNodesStatusPath: '/api/:version/geo_nodes/status',
geoReplicationPath: '/api/:version/geo_replication/:replicable', geoReplicationPath: '/api/:version/geo_replication/:replicable',
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',
...@@ -325,6 +326,16 @@ export default { ...@@ -325,6 +326,16 @@ export default {
return axios.post(url); 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) { createGeoNode(node) {
const url = Api.buildUrl(this.geoNodesPath); const url = Api.buildUrl(this.geoNodesPath);
return axios.post(url, node); return axios.post(url, node);
......
import Vue from 'vue'; import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import GeoNodesBetaApp from './components/app.vue'; import GeoNodesBetaApp from './components/app.vue';
import createStore from './store';
Vue.use(Translate); Vue.use(Translate);
...@@ -11,8 +13,14 @@ export const initGeoNodesBeta = () => { ...@@ -11,8 +13,14 @@ export const initGeoNodesBeta = () => {
return false; return false;
} }
const { primaryVersion, primaryRevision } = el.dataset;
let { replicableTypes } = el.dataset;
replicableTypes = convertObjectPropsToCamelCase(JSON.parse(replicableTypes), { deep: true });
return new Vue({ return new Vue({
el, el,
store: createStore({ primaryVersion, primaryRevision, replicableTypes }),
render(createElement) { render(createElement) {
return createElement(GeoNodesBetaApp); 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 ...@@ -154,6 +154,8 @@ module EE
# Hard Coded Legacy Types, we will want to remove these when they are added to SSF # Hard Coded Legacy Types, we will want to remove these when they are added to SSF
replicable_types = [ replicable_types = [
{ {
data_type: 'repository',
data_type_title: _('Git'),
title: _('Repository'), title: _('Repository'),
title_plural: _('Repositories'), title_plural: _('Repositories'),
name: 'repository', name: 'repository',
...@@ -161,18 +163,24 @@ module EE ...@@ -161,18 +163,24 @@ module EE
secondary_view: true secondary_view: true
}, },
{ {
data_type: 'repository',
data_type_title: _('Git'),
title: _('Wiki'), title: _('Wiki'),
title_plural: _('Wikis'), title_plural: _('Wikis'),
name: 'wiki', name: 'wiki',
name_plural: 'wikis' name_plural: 'wikis'
}, },
{ {
data_type: 'blob',
data_type_title: _('File'),
title: _('LFS object'), title: _('LFS object'),
title_plural: _('LFS objects'), title_plural: _('LFS objects'),
name: 'lfs_object', name: 'lfs_object',
name_plural: 'lfs_objects' name_plural: 'lfs_objects'
}, },
{ {
data_type: 'blob',
data_type_title: _('File'),
title: _('Attachment'), title: _('Attachment'),
title_plural: _('Attachments'), title_plural: _('Attachments'),
name: 'attachment', name: 'attachment',
...@@ -180,18 +188,24 @@ module EE ...@@ -180,18 +188,24 @@ module EE
secondary_view: true secondary_view: true
}, },
{ {
data_type: 'blob',
data_type_title: _('File'),
title: _('Job artifact'), title: _('Job artifact'),
title_plural: _('Job artifacts'), title_plural: _('Job artifacts'),
name: 'job_artifact', name: 'job_artifact',
name_plural: 'job_artifacts' name_plural: 'job_artifacts'
}, },
{ {
data_type: 'blob',
data_type_title: _('File'),
title: _('Container repository'), title: _('Container repository'),
title_plural: _('Container repositories'), title_plural: _('Container repositories'),
name: 'container_repository', name: 'container_repository',
name_plural: 'container_repositories' name_plural: 'container_repositories'
}, },
{ {
data_type: 'repository',
data_type_title: _('Git'),
title: _('Design repository'), title: _('Design repository'),
title_plural: _('Design repositories'), title_plural: _('Design repositories'),
name: 'design_repository', name: 'design_repository',
...@@ -204,6 +218,8 @@ module EE ...@@ -204,6 +218,8 @@ module EE
enabled_replicator_classes.each do |replicator_class| enabled_replicator_classes.each do |replicator_class|
replicable_types.push( replicable_types.push(
{ {
data_type: 'blob',
data_type_title: _('File'),
title: replicator_class.replicable_title, title: replicator_class.replicable_title,
title_plural: replicator_class.replicable_title_plural, title_plural: replicator_class.replicable_title_plural,
name: replicator_class.replicable_name, 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 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Admin::Geo::NodesBetaController do RSpec.describe Admin::Geo::NodesBetaController, :geo do
include AdminModeHelper include AdminModeHelper
let_it_be(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
...@@ -27,7 +27,7 @@ RSpec.describe Admin::Geo::NodesBetaController do ...@@ -27,7 +27,7 @@ RSpec.describe Admin::Geo::NodesBetaController do
get admin_geo_nodes_beta_path get admin_geo_nodes_beta_path
expect(response).to render_template(:index) expect(response).to render_template(:index)
expect(response.body).to include('Geo Nodes Beta') expect(response.body).to include('Geo sites')
end end
end end
......
...@@ -13189,6 +13189,9 @@ msgstr "" ...@@ -13189,6 +13189,9 @@ msgstr ""
msgid "Geo nodes are paused using a command run on the node" msgid "Geo nodes are paused using a command run on the node"
msgstr "" msgstr ""
msgid "Geo sites"
msgstr ""
msgid "GeoNodeStatusEvent|%{timeAgoStr} (%{pendingEvents} events)" msgid "GeoNodeStatusEvent|%{timeAgoStr} (%{pendingEvents} events)"
msgstr "" msgstr ""
...@@ -13567,6 +13570,9 @@ msgstr "" ...@@ -13567,6 +13570,9 @@ msgstr ""
msgid "Getting started with releases" msgid "Getting started with releases"
msgstr "" msgstr ""
msgid "Git"
msgstr ""
msgid "Git LFS is not enabled on this GitLab server, contact your admin." msgid "Git LFS is not enabled on this GitLab server, contact your admin."
msgstr "" msgstr ""
...@@ -29730,6 +29736,9 @@ msgstr "" ...@@ -29730,6 +29736,9 @@ msgstr ""
msgid "There was an error fetching the %{replicableType}" msgid "There was an error fetching the %{replicableType}"
msgstr "" msgstr ""
msgid "There was an error fetching the Geo Nodes"
msgstr ""
msgid "There was an error fetching the Geo Settings" msgid "There was an error fetching the Geo Settings"
msgstr "" 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