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'; ...@@ -3,12 +3,12 @@ import { GlIcon, GlPopover, GlLink, GlButton } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import { GEO_REPLICATION_TYPES_URL } from 'ee/geo_nodes_beta/constants'; import { GEO_REPLICATION_TYPES_URL } from 'ee/geo_nodes_beta/constants';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import GeoNodeReplicationDetailsResponsive from './geo_node_replication_details_responsive.vue';
import GeoNodeReplicationStatusMobile from './geo_node_replication_status_mobile.vue';
export default { export default {
name: 'GeoNodeReplicationDetails', name: 'GeoNodeReplicationDetails',
i18n: { i18n: {
replicationDetailsDesktop: s__('Geo|Replication Details Desktop'),
replicationDetailsMobile: s__('Geo|Replication Details Mobile'),
replicationDetails: s__('Geo|Replication Details'), replicationDetails: s__('Geo|Replication Details'),
popoverText: s__('Geo|Geo supports replication of many data types.'), popoverText: s__('Geo|Geo supports replication of many data types.'),
learnMore: __('Learn more'), learnMore: __('Learn more'),
...@@ -18,6 +18,8 @@ export default { ...@@ -18,6 +18,8 @@ export default {
GlPopover, GlPopover,
GlLink, GlLink,
GlButton, GlButton,
GeoNodeReplicationDetailsResponsive,
GeoNodeReplicationStatusMobile,
}, },
props: { props: {
node: { node: {
...@@ -98,12 +100,25 @@ export default { ...@@ -98,12 +100,25 @@ export default {
</gl-popover> </gl-popover>
</div> </div>
<div v-if="!collapsed"> <div v-if="!collapsed">
<span class="gl-display-none gl-md-display-block" data-testid="replication-details-desktop">{{ <geo-node-replication-details-responsive
$options.i18n.replicationDetailsDesktop class="gl-display-none gl-md-display-block"
}}</span> :replication-items="replicationItems"
<span class="gl-md-display-none!" data-testid="replication-details-mobile">{{ data-testid="geo-replication-details-desktop"
$options.i18n.replicationDetailsMobile />
}}</span> <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>
</div> </div>
</template> </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'; ...@@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import GeoNodeReplicationDetails from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_details.vue'; 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 { 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 { MOCK_NODES, MOCK_REPLICABLE_TYPES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
...@@ -19,7 +21,7 @@ describe('GeoNodeReplicationDetails', () => { ...@@ -19,7 +21,7 @@ describe('GeoNodeReplicationDetails', () => {
const createComponent = (initialState, props, getters) => { const createComponent = (initialState, props, getters) => {
const store = new Vuex.Store({ const store = new Vuex.Store({
state: { state: {
replicableTypes: [], replicableTypes: MOCK_REPLICABLE_TYPES,
...initialState, ...initialState,
}, },
getters: { getters: {
...@@ -36,6 +38,7 @@ describe('GeoNodeReplicationDetails', () => { ...@@ -36,6 +38,7 @@ describe('GeoNodeReplicationDetails', () => {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
stubs: { GeoNodeReplicationDetailsResponsive },
}), }),
); );
}; };
...@@ -44,9 +47,12 @@ describe('GeoNodeReplicationDetails', () => { ...@@ -44,9 +47,12 @@ describe('GeoNodeReplicationDetails', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findGeoMobileReplicationDetails = () => wrapper.findByTestId('replication-details-mobile'); const findGeoMobileReplicationDetails = () =>
wrapper.findByTestId('geo-replication-details-mobile');
const findGeoMobileReplicationStatus = () =>
findGeoMobileReplicationDetails().findComponent(GeoNodeReplicationStatusMobile);
const findGeoDesktopReplicationDetails = () => const findGeoDesktopReplicationDetails = () =>
wrapper.findByTestId('replication-details-desktop'); wrapper.findByTestId('geo-replication-details-desktop');
const findGlIcon = () => wrapper.findComponent(GlIcon); const findGlIcon = () => wrapper.findComponent(GlIcon);
const findGlPopover = () => wrapper.findComponent(GlPopover); const findGlPopover = () => wrapper.findComponent(GlPopover);
const findGlPopoverLink = () => findGlPopover().findComponent(GlLink); const findGlPopoverLink = () => findGlPopover().findComponent(GlLink);
...@@ -88,6 +94,10 @@ describe('GeoNodeReplicationDetails', () => { ...@@ -88,6 +94,10 @@ describe('GeoNodeReplicationDetails', () => {
expect(findGeoMobileReplicationDetails().classes()).toStrictEqual(['gl-md-display-none!']); 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', () => { it('renders desktop details with correct visibility class', () => {
expect(findGeoDesktopReplicationDetails().exists()).toBe(true); expect(findGeoDesktopReplicationDetails().exists()).toBe(true);
expect(findGeoDesktopReplicationDetails().classes()).toStrictEqual([ expect(findGeoDesktopReplicationDetails().classes()).toStrictEqual([
...@@ -158,12 +168,12 @@ describe('GeoNodeReplicationDetails', () => { ...@@ -158,12 +168,12 @@ describe('GeoNodeReplicationDetails', () => {
}; };
describe.each` describe.each`
description | mockSyncData | mockVerificationData | expectedData description | mockSyncData | mockVerificationData | expectedProps
${'with no data'} | ${[]} | ${[]} | ${[mockExpectedNoValues]} ${'with no data'} | ${[]} | ${[]} | ${[mockExpectedNoValues]}
${'with no verification data'} | ${[mockSync]} | ${[]} | ${[mockExpectedOnlySync]} ${'with no verification data'} | ${[mockSync]} | ${[]} | ${[mockExpectedOnlySync]}
${'with no sync data'} | ${[]} | ${[mockVerif]} | ${[mockExpectedOnlyVerif]} ${'with no sync data'} | ${[]} | ${[mockVerif]} | ${[mockExpectedOnlyVerif]}
${'with all data'} | ${[mockSync]} | ${[mockVerif]} | ${[mockExpectedBothTypes]} ${'with all data'} | ${[mockSync]} | ${[mockVerif]} | ${[mockExpectedBothTypes]}
`('$description', ({ mockSyncData, mockVerificationData, expectedData }) => { `('$description', ({ mockSyncData, mockVerificationData, expectedProps }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ replicableTypes: [MOCK_REPLICABLE_TYPES[0]] }, null, { createComponent({ replicableTypes: [MOCK_REPLICABLE_TYPES[0]] }, null, {
syncInfo: () => () => mockSyncData, syncInfo: () => () => mockSyncData,
...@@ -171,9 +181,16 @@ describe('GeoNodeReplicationDetails', () => { ...@@ -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('passes the correct props to the mobile replication details', () => {
it('creates the correct replicationItems array', () => { expect(findGeoMobileReplicationDetails().props('replicationItems')).toStrictEqual(
expect(wrapper.vm.replicationItems).toStrictEqual(expectedData); 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 "" ...@@ -8287,6 +8287,9 @@ msgstr ""
msgid "ComplianceFramework|This project is regulated by %{framework}." msgid "ComplianceFramework|This project is regulated by %{framework}."
msgstr "" msgstr ""
msgid "Component"
msgstr ""
msgid "Confidence" msgid "Confidence"
msgstr "" msgstr ""
...@@ -10354,6 +10357,9 @@ msgstr "" ...@@ -10354,6 +10357,9 @@ msgstr ""
msgid "Data is still calculating..." msgid "Data is still calculating..."
msgstr "" msgstr ""
msgid "Data type"
msgstr ""
msgid "Database update failed" msgid "Database update failed"
msgstr "" msgstr ""
...@@ -14436,6 +14442,9 @@ msgstr "" ...@@ -14436,6 +14442,9 @@ msgstr ""
msgid "GeoNodes|secondary nodes" msgid "GeoNodes|secondary nodes"
msgstr "" msgstr ""
msgid "Geo|%{component} synced"
msgstr ""
msgid "Geo|%{itemTitle} checksum progress" msgid "Geo|%{itemTitle} checksum progress"
msgstr "" msgstr ""
...@@ -14655,16 +14664,13 @@ msgstr "" ...@@ -14655,16 +14664,13 @@ msgstr ""
msgid "Geo|Removing a Geo secondary node stops the synchronization to that node. Are you sure?" msgid "Geo|Removing a Geo secondary node stops the synchronization to that node. Are you sure?"
msgstr "" msgstr ""
msgid "Geo|Replicated data is verified with the secondary node(s) using checksums." msgid "Geo|Replicated data is verified with the secondary node(s) using checksums"
msgstr ""
msgid "Geo|Replication Details"
msgstr "" msgstr ""
msgid "Geo|Replication Details Desktop" msgid "Geo|Replicated data is verified with the secondary node(s) using checksums."
msgstr "" msgstr ""
msgid "Geo|Replication Details Mobile" msgid "Geo|Replication Details"
msgstr "" msgstr ""
msgid "Geo|Replication details" msgid "Geo|Replication details"
...@@ -14733,6 +14739,9 @@ msgstr "" ...@@ -14733,6 +14739,9 @@ msgstr ""
msgid "Geo|Synchronization settings" msgid "Geo|Synchronization settings"
msgstr "" msgstr ""
msgid "Geo|Synchronization status"
msgstr ""
msgid "Geo|The database is currently %{db_lag} behind the primary node." msgid "Geo|The database is currently %{db_lag} behind the primary node."
msgstr "" msgstr ""
...@@ -14778,6 +14787,9 @@ msgstr "" ...@@ -14778,6 +14787,9 @@ msgstr ""
msgid "Geo|Verification failed - %{error}" msgid "Geo|Verification failed - %{error}"
msgstr "" msgstr ""
msgid "Geo|Verification status"
msgstr ""
msgid "Geo|Verificaton information" msgid "Geo|Verificaton information"
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