Commit 65ec1f17 authored by Zack Cuddy's avatar Zack Cuddy

Changes based on @mlunoe feedback

Abstract some logic to child component.
Fix translations
Clean up extendWrapper madness.
Prop cleanup.
parent 0a428380
<script>
import { mapGetters } from 'vuex';
import { REPOSITORY, BLOB } from 'ee/geo_nodes_beta/constants';
import { roundDownFloat } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
import GeoNodeReplicationSyncPercentage from './geo_node_replication_sync_percentage.vue';
export default {
name: 'GeoNodeReplicationCounts',
......@@ -10,71 +10,44 @@ export default {
dataType: s__('Geo|Data type'),
synchronization: s__('Geo|Synchronization'),
verification: s__('Geo|Verification'),
nA: __('N/A'),
git: s__('Geo|Git'),
file: s__('Git|File'),
git: __('Git'),
file: __('File'),
},
components: {
GeoNodeReplicationSyncPercentage,
},
props: {
node: {
type: Object,
nodeId: {
type: Number,
required: true,
},
},
computed: {
...mapGetters(['verificationInfo', 'syncInfo']),
replicationOverview() {
const syncInfoData = this.syncInfo(this.node.id);
const verificationInfoData = this.verificationInfo(this.node.id);
const syncInfoData = this.syncInfo(this.nodeId);
const verificationInfoData = this.verificationInfo(this.nodeId);
const overview = [
return [
{
title: this.$options.i18n.git,
sync: syncInfoData.filter((replicable) => replicable.dataType === REPOSITORY),
verification: verificationInfoData.filter(
(replicable) => replicable.dataType === REPOSITORY,
),
sync: syncInfoData
.filter((replicable) => replicable.dataType === REPOSITORY)
.map((d) => d.values),
verification: verificationInfoData
.filter((replicable) => replicable.dataType === REPOSITORY)
.map((d) => d.values),
},
{
title: this.$options.i18n.file,
sync: syncInfoData.filter((replicable) => replicable.dataType === BLOB),
verification: verificationInfoData.filter((replicable) => replicable.dataType === BLOB),
sync: syncInfoData
.filter((replicable) => replicable.dataType === BLOB)
.map((d) => d.values),
verification: verificationInfoData
.filter((replicable) => replicable.dataType === BLOB)
.map((d) => d.values),
},
];
return overview.map((type) => {
return {
title: type.title,
synchronizationPercent: this.getPercent(type.sync),
verificationPercent: this.getPercent(type.verification),
};
});
},
},
methods: {
getPercent(type) {
// If no data at all, handle as N/A
if (!type.length) {
return null;
}
const total = type.map((t) => t.values.total).reduce((a, b) => a + b);
const success = type.map((t) => t.values.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(value) {
if (value === null) {
return 'gl-bg-gray-200';
}
return value === 100 ? 'gl-bg-green-500' : 'gl-bg-red-500';
},
},
};
......@@ -94,29 +67,8 @@ export default {
data-testid="replication-type"
>
<span class="gl-flex-fill-1" data-testid="replicable-title">{{ type.title }}</span>
<div class="gl-display-flex gl-align-items-center gl-flex-fill-1" data-testid="sync-data">
<div
:class="percentColor(type.synchronizationPercent)"
class="gl-rounded-full gl-w-3 gl-h-3 gl-mr-2"
></div>
<span class="gl-font-weight-bold">{{
type.synchronizationPercent === null
? $options.i18n.nA
: `${type.synchronizationPercent}%`
}}</span>
</div>
<div
class="gl-display-flex gl-align-items-center gl-flex-fill-1"
data-testid="verification-data"
>
<div
:class="percentColor(type.verificationPercent)"
class="gl-rounded-full gl-w-3 gl-h-3 gl-mr-2"
></div>
<span class="gl-font-weight-bold">{{
type.verificationPercent === null ? $options.i18n.nA : `${type.verificationPercent}%`
}}</span>
</div>
<geo-node-replication-sync-percentage :values="type.sync" />
<geo-node-replication-sync-percentage :values="type.verification" />
</div>
</div>
</template>
......@@ -50,6 +50,6 @@ export default {
<span>{{ $options.i18n.syncSettings }}</span>
<geo-node-sync-settings class="gl-mt-2" :node="node" />
</div>
<geo-node-replication-counts :node="node" class="gl-mb-5" />
<geo-node-replication-counts :node-id="node.id" class="gl-mb-5" />
</gl-card>
</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>
......@@ -2,6 +2,7 @@ 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,
......@@ -16,7 +17,7 @@ describe('GeoNodeReplicationCounts', () => {
let wrapper;
const defaultProps = {
node: MOCK_NODES[1],
nodeId: MOCK_NODES[1].id,
};
const createComponent = (props, getters) => {
......@@ -45,11 +46,9 @@ describe('GeoNodeReplicationCounts', () => {
const findReplicationTypeSections = () => wrapper.findAllByTestId('replication-type');
const findReplicationTypeSectionTitles = () =>
findReplicationTypeSections().wrappers.map((w) =>
extendedWrapper(w).findByTestId('replicable-title').text(),
);
const findReplicationTypeSyncData = () => wrapper.findAllByTestId('sync-data');
const findReplicationTypeVerificationData = () => wrapper.findAllByTestId('verification-data');
findReplicationTypeSections().wrappers.map((w) => w.text());
const findGeoNodeReplicationSyncPercentage = () =>
wrapper.findAllComponents(GeoNodeReplicationSyncPercentage);
describe('template', () => {
describe('always', () => {
......@@ -62,85 +61,27 @@ describe('GeoNodeReplicationCounts', () => {
expect(findReplicationTypeSectionTitles()).toStrictEqual(['Git', 'File']);
});
it('renders a sync section for Git and File', () => {
expect(findReplicationTypeSyncData()).toHaveLength(2);
});
it('renders a verification section for Git and File', () => {
expect(findReplicationTypeVerificationData()).toHaveLength(2);
it('renders a sync and verification section for Git and File', () => {
expect(findGeoNodeReplicationSyncPercentage()).toHaveLength(4);
});
});
describe.each`
description | mockGetterData | expectedUI
${'with no data'} | ${[]} | ${{ GIT: { color: 'gl-bg-gray-200', text: 'N/A' }, FILE: { color: 'gl-bg-gray-200', text: 'N/A' } }}
${'with no File data'} | ${[{ dataType: REPOSITORY, values: { total: 100, success: 0 } }]} | ${{ GIT: { color: 'gl-bg-red-500', text: '0%' }, FILE: { color: 'gl-bg-gray-200', text: 'N/A' } }}
${'with no Git data'} | ${[{ dataType: BLOB, values: { total: 100, success: 100 } }]} | ${{ GIT: { color: 'gl-bg-gray-200', text: 'N/A' }, FILE: { color: 'gl-bg-green-500', text: '100%' } }}
${'with all data'} | ${[{ dataType: REPOSITORY, values: { total: 100, success: 0 } }, { dataType: BLOB, values: { total: 100, success: 100 } }]} | ${{ GIT: { color: 'gl-bg-red-500', text: '0%' }, FILE: { color: 'gl-bg-green-500', text: '100%' } }}
${'with malformed data'} | ${[{ dataType: REPOSITORY, values: { total: null, success: 0 } }]} | ${{ GIT: { color: 'gl-bg-gray-200', text: 'N/A' }, FILE: { color: 'gl-bg-gray-200', text: 'N/A' } }}
${'with very small data'} | ${[{ dataType: REPOSITORY, values: { total: 1000, success: 1 } }]} | ${{ GIT: { color: 'gl-bg-red-500', text: '< 1%' }, FILE: { color: 'gl-bg-gray-200', text: 'N/A' } }}
`(`percentages`, ({ description, mockGetterData, expectedUI }) => {
const gitReplicationSection = {};
const fileReplicationSection = {};
description | mockGetterData | expectedData
${'with no data'} | ${[]} | ${[{ title: 'Git', sync: [], verification: [] }, { title: 'File', sync: [], verification: [] }]}
${'with no File data'} | ${[{ dataType: REPOSITORY, values: { total: 100, success: 0 } }]} | ${[{ title: 'Git', sync: [{ total: 100, success: 0 }], verification: [{ total: 100, success: 0 }] }, { title: 'File', sync: [], verification: [] }]}
${'with no Git data'} | ${[{ dataType: BLOB, values: { total: 100, success: 100 } }]} | ${[{ title: 'Git', sync: [], verification: [] }, { title: 'File', sync: [{ total: 100, success: 100 }], verification: [{ total: 100, success: 100 }] }]}
${'with all data'} | ${[{ dataType: REPOSITORY, values: { total: 100, success: 0 } }, { dataType: BLOB, values: { total: 100, success: 100 } }]} | ${[{ title: 'Git', sync: [{ total: 100, success: 0 }], verification: [{ total: 100, success: 0 }] }, { title: 'File', sync: [{ total: 100, success: 100 }], verification: [{ total: 100, success: 100 }] }]}
`('replicationOverview $description', ({ mockGetterData, expectedData }) => {
beforeEach(() => {
createComponent(null, {
syncInfo: () => () => mockGetterData,
verificationInfo: () => () => mockGetterData,
});
gitReplicationSection.syncData = extendedWrapper(
findReplicationTypeSections().at(0),
).findByTestId('sync-data');
gitReplicationSection.verificationData = extendedWrapper(
findReplicationTypeSections().at(0),
).findByTestId('verification-data');
fileReplicationSection.syncData = extendedWrapper(
findReplicationTypeSections().at(1),
).findByTestId('sync-data');
fileReplicationSection.verificationData = extendedWrapper(
findReplicationTypeSections().at(1),
).findByTestId('verification-data');
});
describe(`Git section ${description}`, () => {
it('renders the correct sync data percentage color and text', () => {
expect(
gitReplicationSection.syncData.find('.gl-rounded-full').classes(expectedUI.GIT.color),
).toBe(true);
expect(gitReplicationSection.syncData.find('span').text()).toBe(expectedUI.GIT.text);
});
it('renders the correct verification data percentage color and text', () => {
expect(
gitReplicationSection.verificationData
.find('.gl-rounded-full')
.classes(expectedUI.GIT.color),
).toBe(true);
expect(gitReplicationSection.verificationData.find('span').text()).toBe(
expectedUI.GIT.text,
);
});
});
describe(`File section ${description}`, () => {
it('renders the correct sync data percentage color and text', () => {
expect(
fileReplicationSection.syncData.find('.gl-rounded-full').classes(expectedUI.FILE.color),
).toBe(true);
expect(fileReplicationSection.syncData.find('span').text()).toBe(expectedUI.FILE.text);
});
it('renders the correct verification data percentage color and text', () => {
expect(
fileReplicationSection.verificationData
.find('.gl-rounded-full')
.classes(expectedUI.FILE.color),
).toBe(true);
expect(fileReplicationSection.verificationData.find('span').text()).toBe(
expectedUI.FILE.text,
);
});
it('creates the correct array', () => {
expect(wrapper.vm.replicationOverview).toStrictEqual(expectedData);
});
});
});
......
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);
});
});
});
});
......@@ -14314,9 +14314,6 @@ msgstr ""
msgid "Geo|Geo sites"
msgstr ""
msgid "Geo|Git"
msgstr ""
msgid "Geo|Go to the primary site"
msgstr ""
......@@ -14878,9 +14875,6 @@ msgstr ""
msgid "Gitpod|e.g. https://gitpod.example.com"
msgstr ""
msgid "Git|File"
msgstr ""
msgid "Given access %{time_ago}"
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