Commit 36814cbe authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '287978_09-geo-node-beta-secondary-replication-details-items' into 'master'

Geo Node Status 2.0 - Replication Details

See merge request gitlab-org/gitlab!58121
parents 857a07c5 60e1fbae
......@@ -3,12 +3,12 @@ import { GlIcon, GlPopover, GlLink, GlButton } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import { GEO_REPLICATION_TYPES_URL } from 'ee/geo_nodes_beta/constants';
import { s__, __ } from '~/locale';
import GeoNodeReplicationDetailsResponsive from './geo_node_replication_details_responsive.vue';
import GeoNodeReplicationStatusMobile from './geo_node_replication_status_mobile.vue';
export default {
name: 'GeoNodeReplicationDetails',
i18n: {
replicationDetailsDesktop: s__('Geo|Replication Details Desktop'),
replicationDetailsMobile: s__('Geo|Replication Details Mobile'),
replicationDetails: s__('Geo|Replication Details'),
popoverText: s__('Geo|Geo supports replication of many data types.'),
learnMore: __('Learn more'),
......@@ -18,6 +18,8 @@ export default {
GlPopover,
GlLink,
GlButton,
GeoNodeReplicationDetailsResponsive,
GeoNodeReplicationStatusMobile,
},
props: {
node: {
......@@ -98,12 +100,25 @@ export default {
</gl-popover>
</div>
<div v-if="!collapsed">
<span class="gl-display-none gl-md-display-block" data-testid="replication-details-desktop">{{
$options.i18n.replicationDetailsDesktop
}}</span>
<span class="gl-md-display-none!" data-testid="replication-details-mobile">{{
$options.i18n.replicationDetailsMobile
}}</span>
<geo-node-replication-details-responsive
class="gl-display-none gl-md-display-block"
:replication-items="replicationItems"
data-testid="geo-replication-details-desktop"
/>
<geo-node-replication-details-responsive
class="gl-md-display-none!"
:replication-items="replicationItems"
data-testid="geo-replication-details-mobile"
>
<template #title="{ translations }">
<span class="gl-font-weight-bold">{{ translations.component }}</span>
<span class="gl-font-weight-bold">{{ translations.status }}</span>
</template>
<template #default="{ item, translations }">
<span class="gl-mr-5">{{ item.component }}</span>
<geo-node-replication-status-mobile :item="item" :translations="translations" />
</template>
</geo-node-replication-details-responsive>
</div>
</div>
</template>
<script>
import { GlIcon, GlPopover, GlLink } from '@gitlab/ui';
import GeoNodeProgressBar from 'ee/geo_nodes_beta/components/details/geo_node_progress_bar.vue';
import { HELP_INFO_URL } from 'ee/geo_nodes_beta/constants';
import { s__, __ } from '~/locale';
export default {
name: 'GeoNodeReplicationDetailsResponsive',
i18n: {
dataType: __('Data type'),
component: __('Component'),
status: __('Status'),
syncStatus: s__('Geo|Synchronization status'),
verifStatus: s__('Geo|Verification status'),
popoverHelpText: s__(
'Geo|Replicated data is verified with the secondary node(s) using checksums',
),
learnMore: __('Learn more'),
nA: __('N/A'),
progressBarTitle: s__('Geo|%{component} synced'),
},
components: {
GlIcon,
GlPopover,
GlLink,
GeoNodeProgressBar,
},
props: {
replicationItems: {
type: Array,
required: false,
default: () => [],
},
},
HELP_INFO_URL,
};
</script>
<template>
<div>
<div
class="gl-display-grid geo-node-replication-details-grid-columns gl-bg-gray-10 gl-p-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="replication-details-header"
>
<slot name="title" :translations="$options.i18n">
<span class="gl-font-weight-bold">{{ $options.i18n.dataType }}</span>
<span class="gl-font-weight-bold">{{ $options.i18n.component }}</span>
<span class="gl-font-weight-bold">{{ $options.i18n.syncStatus }}</span>
<div class="gl-display-flex gl-align-items-center">
<span class="gl-font-weight-bold">{{ $options.i18n.verifStatus }}</span>
<gl-icon
ref="verificationStatus"
tabindex="0"
name="question"
class="gl-text-blue-500 gl-cursor-pointer gl-ml-2"
/>
<gl-popover
:target="() => $refs.verificationStatus.$el"
placement="top"
triggers="hover focus"
>
<p class="gl-font-base">
{{ $options.i18n.popoverHelpText }}
</p>
<gl-link :href="$options.HELP_INFO_URL" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
</gl-popover>
</div>
</slot>
</div>
<div
v-for="item in replicationItems"
:key="item.component"
class="gl-display-grid geo-node-replication-details-grid-columns gl-p-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
data-testid="replication-details-item"
>
<slot :item="item" :translations="$options.i18n">
<span class="gl-mr-5">{{ item.dataTypeTitle }}</span>
<span class="gl-mr-5">{{ item.component }}</span>
<div class="gl-mr-5" data-testid="sync-status">
<geo-node-progress-bar
v-if="item.syncValues"
:title="sprintf($options.i18n.progressBarTitle, { component: item.component })"
:values="item.syncValues"
/>
<span v-else class="gl-text-gray-400 gl-font-sm">{{ $options.i18n.nA }}</span>
</div>
<div data-testid="verification-status">
<geo-node-progress-bar
v-if="item.verificationValues"
:title="sprintf($options.i18n.progressBarTitle, { component: item.component })"
:values="item.verificationValues"
/>
<span v-else class="gl-text-gray-400 gl-font-sm">{{ $options.i18n.nA }}</span>
</div>
</slot>
</div>
</div>
</template>
<script>
import GeoNodeProgressBar from 'ee/geo_nodes_beta/components/details/geo_node_progress_bar.vue';
export default {
name: 'GeoNodeReplicationStatusMobile',
components: {
GeoNodeProgressBar,
},
props: {
item: {
type: Object,
required: true,
},
translations: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div>
<div class="gl-mb-5 gl-display-flex gl-flex-direction-column" data-testid="sync-status">
<span class="gl-font-sm gl-mb-3">{{ translations.syncStatus }}</span>
<geo-node-progress-bar
v-if="item.syncValues"
:title="sprintf(translations.progressBarTitle, { component: item.component })"
:values="item.syncValues"
/>
<span v-else class="gl-text-gray-400 gl-font-sm">{{ translations.nA }}</span>
</div>
<div class="gl-display-flex gl-flex-direction-column" data-testid="verification-status">
<span class="gl-font-sm gl-mb-3">{{ translations.verifStatus }}</span>
<geo-node-progress-bar
v-if="item.verificationValues"
:title="sprintf(translations.progressBarTitle, { component: item.component })"
:values="item.verificationValues"
/>
<span v-else class="gl-text-gray-400 gl-font-sm">{{ translations.nA }}</span>
</div>
</div>
</template>
import { GlIcon, GlPopover, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import GeoNodeProgressBar from 'ee/geo_nodes_beta/components/details/geo_node_progress_bar.vue';
import GeoNodeReplicationDetailsResponsive from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_details_responsive.vue';
import { HELP_INFO_URL } from 'ee/geo_nodes_beta/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeReplicationDetailsResponsive', () => {
let wrapper;
const defaultProps = {
replicationItems: [],
};
const createComponent = (props, slots) => {
wrapper = extendedWrapper(
shallowMount(GeoNodeReplicationDetailsResponsive, {
propsData: {
...defaultProps,
...props,
},
scopedSlots: {
...slots,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findGlPopover = () => wrapper.findComponent(GlPopover);
const findGlPopoverLink = () => findGlPopover().findComponent(GlLink);
const findReplicationDetailsHeader = () => wrapper.findByTestId('replication-details-header');
const findReplicationDetailsItems = () => wrapper.findAllByTestId('replication-details-item');
const findFirstReplicationDetailsItemSyncStatus = () =>
extendedWrapper(findReplicationDetailsItems().at(0)).findByTestId('sync-status');
const findFirstReplicationDetailsItemVerifStatus = () =>
extendedWrapper(findReplicationDetailsItems().at(0)).findByTestId('verification-status');
describe('template', () => {
describe('with default slots', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the replication details header', () => {
expect(findReplicationDetailsHeader().exists()).toBe(true);
});
it('renders the replication details header items correctly', () => {
expect(findReplicationDetailsHeader().text()).toContain(
'Data type Component Synchronization status Verification status',
);
});
it('renders the question icon correctly', () => {
expect(findGlIcon().exists()).toBe(true);
expect(findGlIcon().props('name')).toBe('question');
});
it('renders the GlPopover always', () => {
expect(findGlPopover().exists()).toBe(true);
});
it('renders the popover link correctly', () => {
expect(findGlPopoverLink().exists()).toBe(true);
expect(findGlPopoverLink().attributes('href')).toBe(HELP_INFO_URL);
});
});
describe('replication details', () => {
describe('when null', () => {
beforeEach(() => {
createComponent({ replicationItems: null });
});
it('does not render any replicable items', () => {
expect(findReplicationDetailsItems()).toHaveLength(0);
});
});
});
describe.each`
description | replicationItems | renderSyncProgress | renderVerifProgress
${'with no data'} | ${[{ dataTypeTitle: 'Test Title', component: 'Test Component', syncValues: null, verificationValues: null }]} | ${false} | ${false}
${'with no verification data'} | ${[{ dataTypeTitle: 'Test Title', component: 'Test Component', syncValues: { total: 100, success: 0 }, verificationValues: null }]} | ${true} | ${false}
${'with no sync data'} | ${[{ dataTypeTitle: 'Test Title', component: 'Test Component', syncValues: null, verificationValues: { total: 50, success: 50 } }]} | ${false} | ${true}
${'with all data'} | ${[{ dataTypeTitle: 'Test Title', component: 'Test Component', syncValues: { total: 100, success: 0 }, verificationValues: { total: 50, success: 50 } }]} | ${true} | ${true}
`('$description', ({ replicationItems, renderSyncProgress, renderVerifProgress }) => {
beforeEach(() => {
createComponent({ replicationItems });
});
it('renders sync progress correctly', () => {
expect(
findFirstReplicationDetailsItemSyncStatus().find(GeoNodeProgressBar).exists(),
).toBe(renderSyncProgress);
expect(
extendedWrapper(findFirstReplicationDetailsItemSyncStatus()).findByText('N/A').exists(),
).toBe(!renderSyncProgress);
});
it('renders verification progress correctly', () => {
expect(
findFirstReplicationDetailsItemVerifStatus().find(GeoNodeProgressBar).exists(),
).toBe(renderVerifProgress);
expect(
extendedWrapper(findFirstReplicationDetailsItemVerifStatus())
.findByText('N/A')
.exists(),
).toBe(!renderVerifProgress);
});
});
});
describe('with custom title slot', () => {
beforeEach(() => {
const title =
'<template #title="{ translations }"><span>{{ translations.component }} {{ translations.status }}</span></template>';
createComponent(null, { title });
});
it('renders the replication details header', () => {
expect(findReplicationDetailsHeader().exists()).toBe(true);
});
it('renders the replication details header with access to the translations prop', () => {
expect(findReplicationDetailsHeader().text()).toBe('Component Status');
});
});
describe('with custom default slot', () => {
beforeEach(() => {
const defaultSlot =
'<template #default="{ item, translations }"><span>{{ item.component }} {{ item.dataTypeTitle }} {{ translations.status }}</span></template>';
createComponent(
{ replicationItems: [{ component: 'Test Component', dataTypeTitle: 'Test Title' }] },
{ default: defaultSlot },
);
});
it('renders the replication details items section', () => {
expect(findReplicationDetailsItems().exists()).toBe(true);
});
it('renders the replication details items section with access to the item and translations prop', () => {
expect(findReplicationDetailsItems().at(0).text()).toBe('Test Component Test Title Status');
});
});
});
});
......@@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import GeoNodeReplicationDetails from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_details.vue';
import GeoNodeReplicationDetailsResponsive from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_details_responsive.vue';
import GeoNodeReplicationStatusMobile from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status_mobile.vue';
import { GEO_REPLICATION_TYPES_URL } from 'ee/geo_nodes_beta/constants';
import { MOCK_NODES, MOCK_REPLICABLE_TYPES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -19,7 +21,7 @@ describe('GeoNodeReplicationDetails', () => {
const createComponent = (initialState, props, getters) => {
const store = new Vuex.Store({
state: {
replicableTypes: [],
replicableTypes: MOCK_REPLICABLE_TYPES,
...initialState,
},
getters: {
......@@ -36,6 +38,7 @@ describe('GeoNodeReplicationDetails', () => {
...defaultProps,
...props,
},
stubs: { GeoNodeReplicationDetailsResponsive },
}),
);
};
......@@ -44,9 +47,12 @@ describe('GeoNodeReplicationDetails', () => {
wrapper.destroy();
});
const findGeoMobileReplicationDetails = () => wrapper.findByTestId('replication-details-mobile');
const findGeoMobileReplicationDetails = () =>
wrapper.findByTestId('geo-replication-details-mobile');
const findGeoMobileReplicationStatus = () =>
findGeoMobileReplicationDetails().findComponent(GeoNodeReplicationStatusMobile);
const findGeoDesktopReplicationDetails = () =>
wrapper.findByTestId('replication-details-desktop');
wrapper.findByTestId('geo-replication-details-desktop');
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findGlPopover = () => wrapper.findComponent(GlPopover);
const findGlPopoverLink = () => findGlPopover().findComponent(GlLink);
......@@ -88,6 +94,10 @@ describe('GeoNodeReplicationDetails', () => {
expect(findGeoMobileReplicationDetails().classes()).toStrictEqual(['gl-md-display-none!']);
});
it('renders mobile replication details with mobile component slot', () => {
expect(findGeoMobileReplicationStatus().exists()).toBe(true);
});
it('renders desktop details with correct visibility class', () => {
expect(findGeoDesktopReplicationDetails().exists()).toBe(true);
expect(findGeoDesktopReplicationDetails().classes()).toStrictEqual([
......@@ -158,12 +168,12 @@ describe('GeoNodeReplicationDetails', () => {
};
describe.each`
description | mockSyncData | mockVerificationData | expectedData
description | mockSyncData | mockVerificationData | expectedProps
${'with no data'} | ${[]} | ${[]} | ${[mockExpectedNoValues]}
${'with no verification data'} | ${[mockSync]} | ${[]} | ${[mockExpectedOnlySync]}
${'with no sync data'} | ${[]} | ${[mockVerif]} | ${[mockExpectedOnlyVerif]}
${'with all data'} | ${[mockSync]} | ${[mockVerif]} | ${[mockExpectedBothTypes]}
`('$description', ({ mockSyncData, mockVerificationData, expectedData }) => {
`('$description', ({ mockSyncData, mockVerificationData, expectedProps }) => {
beforeEach(() => {
createComponent({ replicableTypes: [MOCK_REPLICABLE_TYPES[0]] }, null, {
syncInfo: () => () => mockSyncData,
......@@ -171,9 +181,16 @@ describe('GeoNodeReplicationDetails', () => {
});
});
// TODO: Replace this spec with a template spec, once the UI has been hooked up in the next MR.
it('creates the correct replicationItems array', () => {
expect(wrapper.vm.replicationItems).toStrictEqual(expectedData);
it('passes the correct props to the mobile replication details', () => {
expect(findGeoMobileReplicationDetails().props('replicationItems')).toStrictEqual(
expectedProps,
);
});
it('passes the correct props to the desktop replication details', () => {
expect(findGeoDesktopReplicationDetails().props('replicationItems')).toStrictEqual(
expectedProps,
);
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import GeoNodeProgressBar from 'ee/geo_nodes_beta/components/details/geo_node_progress_bar.vue';
import GeoNodeReplicationStatusMobile from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status_mobile.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeReplicationStatusMobile', () => {
let wrapper;
const defaultProps = {
item: {
component: 'Test',
syncValues: null,
verificationValues: null,
},
translations: {
nA: 'N/A',
progressBarTitle: '%{component} synced',
},
};
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(GeoNodeReplicationStatusMobile, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findItemSyncStatus = () => wrapper.findByTestId('sync-status');
const findItemVerificationStatus = () => wrapper.findByTestId('verification-status');
describe('template', () => {
describe.each`
description | item | renderSyncProgress | renderVerifProgress
${'with no data'} | ${{ component: 'Test Component', syncValues: null, verificationValues: null }} | ${false} | ${false}
${'with no verification data'} | ${{ component: 'Test Component', syncValues: { total: 100, success: 0 }, verificationValues: null }} | ${true} | ${false}
${'with no sync data'} | ${{ component: 'Test Component', syncValues: null, verificationValues: { total: 50, success: 50 } }} | ${false} | ${true}
${'with all data'} | ${{ component: 'Test Component', syncValues: { total: 100, success: 0 }, verificationValues: { total: 50, success: 50 } }} | ${true} | ${true}
`('$description', ({ item, renderSyncProgress, renderVerifProgress }) => {
beforeEach(() => {
createComponent({ item });
});
it('renders sync progress correctly', () => {
expect(findItemSyncStatus().find(GeoNodeProgressBar).exists()).toBe(renderSyncProgress);
expect(extendedWrapper(findItemSyncStatus()).findByText('N/A').exists()).toBe(
!renderSyncProgress,
);
});
it('renders verification progress correctly', () => {
expect(findItemVerificationStatus().find(GeoNodeProgressBar).exists()).toBe(
renderVerifProgress,
);
expect(extendedWrapper(findItemVerificationStatus()).findByText('N/A').exists()).toBe(
!renderVerifProgress,
);
});
});
});
});
......@@ -8287,6 +8287,9 @@ msgstr ""
msgid "ComplianceFramework|This project is regulated by %{framework}."
msgstr ""
msgid "Component"
msgstr ""
msgid "Confidence"
msgstr ""
......@@ -10354,6 +10357,9 @@ msgstr ""
msgid "Data is still calculating..."
msgstr ""
msgid "Data type"
msgstr ""
msgid "Database update failed"
msgstr ""
......@@ -14436,6 +14442,9 @@ msgstr ""
msgid "GeoNodes|secondary nodes"
msgstr ""
msgid "Geo|%{component} synced"
msgstr ""
msgid "Geo|%{itemTitle} checksum progress"
msgstr ""
......@@ -14655,16 +14664,13 @@ msgstr ""
msgid "Geo|Removing a Geo secondary node stops the synchronization to that node. Are you sure?"
msgstr ""
msgid "Geo|Replicated data is verified with the secondary node(s) using checksums."
msgstr ""
msgid "Geo|Replication Details"
msgid "Geo|Replicated data is verified with the secondary node(s) using checksums"
msgstr ""
msgid "Geo|Replication Details Desktop"
msgid "Geo|Replicated data is verified with the secondary node(s) using checksums."
msgstr ""
msgid "Geo|Replication Details Mobile"
msgid "Geo|Replication Details"
msgstr ""
msgid "Geo|Replication details"
......@@ -14733,6 +14739,9 @@ msgstr ""
msgid "Geo|Synchronization settings"
msgstr ""
msgid "Geo|Synchronization status"
msgstr ""
msgid "Geo|The database is currently %{db_lag} behind the primary node."
msgstr ""
......@@ -14778,6 +14787,9 @@ msgstr ""
msgid "Geo|Verification failed - %{error}"
msgstr ""
msgid "Geo|Verification status"
msgstr ""
msgid "Geo|Verificaton information"
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