Commit d140c45f authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '287978_07_05-geo-node-beta-secondary-replication-counts' into 'master'

Geo Node Status 2.0 - Replication Counts

See merge request gitlab-org/gitlab!57507
parents 60673b28 4fa4bff8
<script>
import { mapGetters } from 'vuex';
import { REPOSITORY, BLOB } from 'ee/geo_nodes_beta/constants';
import { __, s__ } from '~/locale';
import GeoNodeReplicationSyncPercentage from './geo_node_replication_sync_percentage.vue';
export default {
name: 'GeoNodeReplicationCounts',
i18n: {
dataType: s__('Geo|Data type'),
synchronization: s__('Geo|Synchronization'),
verification: s__('Geo|Verification'),
git: __('Git'),
file: __('File'),
},
components: {
GeoNodeReplicationSyncPercentage,
},
props: {
nodeId: {
type: Number,
required: true,
},
},
computed: {
...mapGetters(['verificationInfo', 'syncInfo']),
replicationOverview() {
const syncInfoData = this.syncInfo(this.nodeId);
const verificationInfoData = this.verificationInfo(this.nodeId);
return [
{
title: this.$options.i18n.git,
sync: this.filterByDataType(syncInfoData, REPOSITORY),
verification: this.filterByDataType(verificationInfoData, REPOSITORY),
},
{
title: this.$options.i18n.file,
sync: this.filterByDataType(syncInfoData, BLOB),
verification: this.filterByDataType(verificationInfoData, BLOB),
},
];
},
},
methods: {
filterByDataType(data, type) {
return data.filter((replicable) => replicable.dataType === type).map((d) => d.values);
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-align-items-center gl-mb-3">
<span class="gl-flex-fill-1">{{ $options.i18n.dataType }}</span>
<span class="gl-flex-fill-1">{{ $options.i18n.synchronization }}</span>
<span class="gl-flex-fill-1">{{ $options.i18n.verification }}</span>
</div>
<div
v-for="type in replicationOverview"
:key="type.title"
class="gl-display-flex gl-align-items-center gl-mb-3"
data-testid="replication-type"
>
<span class="gl-flex-fill-1" data-testid="replicable-title">{{ type.title }}</span>
<geo-node-replication-sync-percentage :values="type.sync" />
<geo-node-replication-sync-percentage :values="type.verification" />
</div>
</div>
</template>
<script> <script>
import { GlCard, GlButton } from '@gitlab/ui'; import { GlCard, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import GeoNodeReplicationCounts from './geo_node_replication_counts.vue';
import GeoNodeReplicationStatus from './geo_node_replication_status.vue'; import GeoNodeReplicationStatus from './geo_node_replication_status.vue';
import GeoNodeSyncSettings from './geo_node_sync_settings.vue'; import GeoNodeSyncSettings from './geo_node_sync_settings.vue';
...@@ -11,13 +12,13 @@ export default { ...@@ -11,13 +12,13 @@ export default {
replicationDetailsButton: s__('Geo|Replication details'), replicationDetailsButton: s__('Geo|Replication details'),
replicationStatus: s__('Geo|Replication status'), replicationStatus: s__('Geo|Replication status'),
syncSettings: s__('Geo|Synchronization settings'), syncSettings: s__('Geo|Synchronization settings'),
replicationCounts: s__('Geo|Replication counts'),
}, },
components: { components: {
GlCard, GlCard,
GlButton, GlButton,
GeoNodeReplicationStatus, GeoNodeReplicationStatus,
GeoNodeSyncSettings, GeoNodeSyncSettings,
GeoNodeReplicationCounts,
}, },
props: { props: {
node: { node: {
...@@ -49,6 +50,6 @@ export default { ...@@ -49,6 +50,6 @@ export default {
<span>{{ $options.i18n.syncSettings }}</span> <span>{{ $options.i18n.syncSettings }}</span>
<geo-node-sync-settings class="gl-mt-2" :node="node" /> <geo-node-sync-settings class="gl-mt-2" :node="node" />
</div> </div>
<span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span> <geo-node-replication-counts :node-id="node.id" class="gl-mb-5" />
</gl-card> </gl-card>
</template> </template>
<script>
import { roundDownFloat } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
export default {
name: 'GeoNodeReplicationSyncPercentage',
i18n: {
nA: __('N/A'),
},
props: {
values: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
percent() {
if (!this.values.length) {
return null;
}
const total = this.values.map((v) => v.total).reduce((a, b) => a + b);
const success = this.values.map((v) => v.success).reduce((a, b) => a + b);
const percent = roundDownFloat((success / total) * 100, 1);
if (percent > 0 && percent < 1) {
// Special case for very small numbers
return '< 1';
}
// If total/success has any null values it will return NaN, lets render N/A for this case too.
return Number.isNaN(percent) ? null : percent;
},
percentColor() {
if (this.percent === null) {
return 'gl-bg-gray-200';
}
return this.percent === 100 ? 'gl-bg-green-500' : 'gl-bg-red-500';
},
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center gl-flex-fill-1">
<div :class="percentColor" class="gl-rounded-full gl-w-3 gl-h-3 gl-mr-2"></div>
<span class="gl-font-weight-bold">
{{ percent === null ? $options.i18n.nA : `${percent}%` }}
</span>
</div>
</template>
...@@ -59,3 +59,7 @@ export const REPLICATION_STATUS_UI = { ...@@ -59,3 +59,7 @@ export const REPLICATION_STATUS_UI = {
}; };
export const STATUS_DELAY_THRESHOLD_MS = 600000; export const STATUS_DELAY_THRESHOLD_MS = 600000;
export const REPOSITORY = 'repository';
export const BLOB = 'blob';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import GeoNodeReplicationCounts from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_counts.vue';
import GeoNodeReplicationSyncPercentage from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_sync_percentage.vue';
import { REPOSITORY, BLOB } from 'ee/geo_nodes_beta/constants';
import {
MOCK_NODES,
MOCK_SECONDARY_SYNC_INFO,
MOCK_PRIMARY_VERIFICATION_INFO,
} from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.use(Vuex);
describe('GeoNodeReplicationCounts', () => {
let wrapper;
const defaultProps = {
nodeId: MOCK_NODES[1].id,
};
const createComponent = (props, getters) => {
const store = new Vuex.Store({
getters: {
syncInfo: () => () => MOCK_SECONDARY_SYNC_INFO,
verificationInfo: () => () => MOCK_PRIMARY_VERIFICATION_INFO,
...getters,
},
});
wrapper = extendedWrapper(
shallowMount(GeoNodeReplicationCounts, {
store,
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findReplicationTypeSections = () => wrapper.findAllByTestId('replication-type');
const findReplicationTypeSectionTitles = () =>
findReplicationTypeSections().wrappers.map((w) => w.text());
const findGeoNodeReplicationSyncPercentage = () =>
wrapper.findAllComponents(GeoNodeReplicationSyncPercentage);
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders a replication type section for Git and File', () => {
expect(findReplicationTypeSections()).toHaveLength(2);
expect(findReplicationTypeSectionTitles()).toStrictEqual(['Git', 'File']);
});
it('renders a sync and verification section for Git and File', () => {
expect(findGeoNodeReplicationSyncPercentage()).toHaveLength(4);
});
});
const mockRepositoryData = { dataType: REPOSITORY, values: { total: 100, success: 0 } };
const mockBlobData = { dataType: BLOB, values: { total: 100, success: 100 } };
const mockGitEmptySync = { title: 'Git', sync: [], verification: [] };
const mockGitSuccess0 = {
title: 'Git',
sync: [{ total: 100, success: 0 }],
verification: [{ total: 100, success: 0 }],
};
const mockFileEmptySync = { title: 'File', sync: [], verification: [] };
const mockFileSync100 = {
title: 'File',
sync: [{ total: 100, success: 100 }],
verification: [{ total: 100, success: 100 }],
};
describe.each`
description | mockGetterData | expectedData
${'with no data'} | ${[]} | ${[mockGitEmptySync, mockFileEmptySync]}
${'with no File data'} | ${[mockRepositoryData]} | ${[mockGitSuccess0, mockFileEmptySync]}
${'with no Git data'} | ${[mockBlobData]} | ${[mockGitEmptySync, mockFileSync100]}
${'with all data'} | ${[mockRepositoryData, mockBlobData]} | ${[mockGitSuccess0, mockFileSync100]}
`('replicationOverview $description', ({ mockGetterData, expectedData }) => {
beforeEach(() => {
createComponent(null, {
syncInfo: () => () => mockGetterData,
verificationInfo: () => () => mockGetterData,
});
});
it('creates the correct array', () => {
expect(wrapper.vm.replicationOverview).toStrictEqual(expectedData);
});
});
});
});
import { GlButton, GlCard } from '@gitlab/ui'; import { GlButton, GlCard } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import GeoNodeReplicationCounts from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_counts.vue';
import GeoNodeReplicationStatus from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status.vue'; import GeoNodeReplicationStatus from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status.vue';
import GeoNodeReplicationSummary from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_summary.vue'; import GeoNodeReplicationSummary from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_summary.vue';
import GeoNodeSyncSettings from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_sync_settings.vue'; import GeoNodeSyncSettings from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_sync_settings.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data'; import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeReplicationSummary', () => { describe('GeoNodeReplicationSummary', () => {
let wrapper; let wrapper;
...@@ -14,15 +14,13 @@ describe('GeoNodeReplicationSummary', () => { ...@@ -14,15 +14,13 @@ describe('GeoNodeReplicationSummary', () => {
}; };
const createComponent = (props) => { const createComponent = (props) => {
wrapper = extendedWrapper( wrapper = shallowMount(GeoNodeReplicationSummary, {
shallowMount(GeoNodeReplicationSummary, { propsData: {
propsData: { ...defaultProps,
...defaultProps, ...props,
...props, },
}, stubs: { GlCard },
stubs: { GlCard }, });
}),
);
}; };
afterEach(() => { afterEach(() => {
...@@ -31,7 +29,7 @@ describe('GeoNodeReplicationSummary', () => { ...@@ -31,7 +29,7 @@ describe('GeoNodeReplicationSummary', () => {
const findGlButton = () => wrapper.findComponent(GlButton); const findGlButton = () => wrapper.findComponent(GlButton);
const findGeoNodeReplicationStatus = () => wrapper.findComponent(GeoNodeReplicationStatus); const findGeoNodeReplicationStatus = () => wrapper.findComponent(GeoNodeReplicationStatus);
const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts'); const findGeoNodeReplicationCounts = () => wrapper.findComponent(GeoNodeReplicationCounts);
const findGeoNodeSyncSettings = () => wrapper.findComponent(GeoNodeSyncSettings); const findGeoNodeSyncSettings = () => wrapper.findComponent(GeoNodeSyncSettings);
describe('template', () => { describe('template', () => {
......
import { shallowMount } from '@vue/test-utils';
import GeoNodeReplicationSyncPercentage from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_sync_percentage.vue';
describe('GeoNodeReplicationSyncPercentage', () => {
let wrapper;
const defaultProps = {
values: [],
};
const createComponent = (props) => {
wrapper = shallowMount(GeoNodeReplicationSyncPercentage, {
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findPercentageIndicator = () => wrapper.find('.gl-rounded-full');
const findPercentage = () => wrapper.find('span');
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the percentage indicator', () => {
expect(findPercentageIndicator().exists()).toBe(true);
});
it('renders the percentage number', () => {
expect(findPercentage().exists()).toBe(true);
});
});
describe.each`
description | values | expectedColor | expectedText
${'with no data'} | ${[]} | ${'gl-bg-gray-200'} | ${'N/A'}
${'with all success'} | ${[{ total: 100, success: 100 }]} | ${'gl-bg-green-500'} | ${'100%'}
${'with all failure'} | ${[{ total: 100, success: 0 }]} | ${'gl-bg-red-500'} | ${'0%'}
${'with multiple data'} | ${[{ total: 100, success: 100 }, { total: 100, success: 0 }]} | ${'gl-bg-red-500'} | ${'50%'}
${'with malformed data'} | ${[{ total: null, success: 0 }]} | ${'gl-bg-gray-200'} | ${'N/A'}
${'with very small data'} | ${[{ total: 1000, success: 1 }]} | ${'gl-bg-red-500'} | ${'< 1%'}
`('conditionally $description', ({ values, expectedColor, expectedText }) => {
beforeEach(() => {
createComponent({ values });
});
it('renders the correct percentage color', () => {
expect(findPercentageIndicator().classes(expectedColor)).toBe(true);
});
it('renders the correct percentage text', () => {
expect(findPercentage().text()).toBe(expectedText);
});
});
});
});
...@@ -14284,6 +14284,9 @@ msgstr "" ...@@ -14284,6 +14284,9 @@ msgstr ""
msgid "Geo|Data replication lag" msgid "Geo|Data replication lag"
msgstr "" msgstr ""
msgid "Geo|Data type"
msgstr ""
msgid "Geo|Discover GitLab Geo" msgid "Geo|Discover GitLab Geo"
msgstr "" msgstr ""
...@@ -14413,9 +14416,6 @@ msgstr "" ...@@ -14413,9 +14416,6 @@ msgstr ""
msgid "Geo|Replication Details" msgid "Geo|Replication Details"
msgstr "" msgstr ""
msgid "Geo|Replication counts"
msgstr ""
msgid "Geo|Replication details" msgid "Geo|Replication details"
msgstr "" msgstr ""
...@@ -14470,6 +14470,9 @@ msgstr "" ...@@ -14470,6 +14470,9 @@ msgstr ""
msgid "Geo|Synced at" msgid "Geo|Synced at"
msgstr "" msgstr ""
msgid "Geo|Synchronization"
msgstr ""
msgid "Geo|Synchronization failed - %{error}" msgid "Geo|Synchronization failed - %{error}"
msgstr "" msgstr ""
...@@ -14509,6 +14512,9 @@ msgstr "" ...@@ -14509,6 +14512,9 @@ msgstr ""
msgid "Geo|Unknown state" msgid "Geo|Unknown state"
msgstr "" msgstr ""
msgid "Geo|Verification"
msgstr ""
msgid "Geo|Verification failed - %{error}" msgid "Geo|Verification failed - %{error}"
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