Commit 90f66f74 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch '329792-geo-beta-handle-null-better' into 'master'

Geo 2.0 Regression - No Data Errors

See merge request gitlab-org/gitlab!61042
parents 004baec9 75ad6555
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
otherInformation: __('Other information'), otherInformation: __('Other information'),
replicationSlotWAL: s__('Geo|Replication slot WAL'), replicationSlotWAL: s__('Geo|Replication slot WAL'),
replicationSlots: s__('Geo|Replication slots'), replicationSlots: s__('Geo|Replication slots'),
unknown: __('Unknown'),
}, },
components: { components: {
GlCard, GlCard,
...@@ -23,7 +24,9 @@ export default { ...@@ -23,7 +24,9 @@ export default {
}, },
computed: { computed: {
replicationSlotWAL() { replicationSlotWAL() {
return numberToHumanSize(this.node.replicationSlotsMaxRetainedWalBytes); return this.node.replicationSlotsMaxRetainedWalBytes
? numberToHumanSize(this.node.replicationSlotsMaxRetainedWalBytes)
: this.$options.i18n.unknown;
}, },
replicationSlots() { replicationSlots() {
return { return {
...@@ -51,10 +54,7 @@ export default { ...@@ -51,10 +54,7 @@ export default {
:values="replicationSlots.values" :values="replicationSlots.values"
/> />
</div> </div>
<div <div class="gl-display-flex gl-flex-direction-column gl-mb-5">
v-if="node.replicationSlotsMaxRetainedWalBytes"
class="gl-display-flex gl-flex-direction-column gl-mb-5"
>
<span>{{ $options.i18n.replicationSlotWAL }}</span> <span>{{ $options.i18n.replicationSlotWAL }}</span>
<span class="gl-font-weight-bold gl-mt-2" data-testid="replication-slot-wal">{{ <span class="gl-font-weight-bold gl-mt-2" data-testid="replication-slot-wal">{{
replicationSlotWAL replicationSlotWAL
......
<script> <script>
import { GlCard } from '@gitlab/ui'; import { GlCard, GlSprintf } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
otherInfo: __('Other information'), otherInfo: __('Other information'),
dbReplicationLag: s__('Geo|Data replication lag'), dbReplicationLag: s__('Geo|Data replication lag'),
lastEventId: s__('Geo|Last event ID from primary'), lastEventId: s__('Geo|Last event ID from primary'),
lastEvent: s__('Geo|%{eventId} (%{timeAgo})'),
lastCursorEventId: s__('Geo|Last event ID processed by cursor'), lastCursorEventId: s__('Geo|Last event ID processed by cursor'),
storageConfig: s__('Geo|Storage config'), storageConfig: s__('Geo|Storage config'),
shardsNotMatched: s__('Geo|Does not match the primary storage configuration'), shardsNotMatched: s__('Geo|Does not match the primary storage configuration'),
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
}, },
components: { components: {
GlCard, GlCard,
GlSprintf,
TimeAgo, TimeAgo,
}, },
props: { props: {
...@@ -73,30 +75,37 @@ export default { ...@@ -73,30 +75,37 @@ export default {
</div> </div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5"> <div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span>{{ $options.i18n.lastEventId }}</span> <span>{{ $options.i18n.lastEventId }}</span>
<span class="gl-font-weight-bold gl-mt-2" <span class="gl-font-weight-bold gl-mt-2" data-testid="last-event">
>{{ node.lastEventId || 0 }} (<time-ago <gl-sprintf v-if="node.lastEventId" :message="$options.i18n.lastEvent">
data-testid="last-event" <template #eventId>
:time="lastEventTimestamp" {{ node.lastEventId }}
/>)</span </template>
> <template #timeAgo>
<time-ago :time="lastEventTimestamp" />
</template>
</gl-sprintf>
<span v-else>{{ $options.i18n.unknown }}</span>
</span>
</div> </div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5"> <div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span>{{ $options.i18n.lastCursorEventId }}</span> <span>{{ $options.i18n.lastCursorEventId }}</span>
<span class="gl-font-weight-bold gl-mt-2" <span class="gl-font-weight-bold gl-mt-2" data-testid="last-cursor-event">
>{{ node.cursorLastEventId || 0 }} (<time-ago <gl-sprintf v-if="node.cursorLastEventId" :message="$options.i18n.lastEvent">
data-testid="last-cursor-event" <template #eventId>
:time="lastCursorEventTimestamp" {{ node.cursorLastEventId }}
/>)</span </template>
> <template #timeAgo>
<time-ago :time="lastCursorEventTimestamp" />
</template>
</gl-sprintf>
<span v-else>{{ $options.i18n.unknown }}</span>
</span>
</div> </div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5"> <div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span>{{ $options.i18n.storageConfig }}</span> <span>{{ $options.i18n.storageConfig }}</span>
<span <span class="gl-font-weight-bold gl-mt-2" data-testid="storage-shards">{{
:class="{ 'gl-text-red-500': !node.storageShardsMatch }" storageShardsStatus
class="gl-font-weight-bold gl-mt-2" }}</span>
data-testid="storage-shards"
>{{ storageShardsStatus }}</span
>
</div> </div>
</gl-card> </gl-card>
</template> </template>
...@@ -17,8 +17,7 @@ export default { ...@@ -17,8 +17,7 @@ export default {
node: { node: {
type: Object, type: Object,
required: true, required: true,
validator: (value) => validator: (value) => ['id', 'name', 'url'].every((prop) => value[prop]),
['id', 'name', 'geoNodeId', 'url', 'healthStatus'].every((prop) => value[prop]),
}, },
}, },
data() { data() {
......
...@@ -38,7 +38,9 @@ export default { ...@@ -38,7 +38,9 @@ export default {
return this.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse; return this.collapsed ? this.$options.i18n.expand : this.$options.i18n.collapse;
}, },
statusCheckTimestamp() { statusCheckTimestamp() {
return this.node.lastSuccessfulStatusCheckTimestamp * 1000; return this.node.lastSuccessfulStatusCheckTimestamp
? this.node.lastSuccessfulStatusCheckTimestamp * 1000
: null;
}, },
}, },
}; };
...@@ -68,7 +70,11 @@ export default { ...@@ -68,7 +70,11 @@ export default {
</div> </div>
<div class="gl-display-flex gl-align-items-center gl-flex-fill-2"> <div class="gl-display-flex gl-align-items-center gl-flex-fill-2">
<geo-node-health-status :status="node.healthStatus" /> <geo-node-health-status :status="node.healthStatus" />
<geo-node-last-updated class="gl-ml-2" :status-check-timestamp="statusCheckTimestamp" /> <geo-node-last-updated
v-if="statusCheckTimestamp"
class="gl-ml-2"
:status-check-timestamp="statusCheckTimestamp"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -10,12 +10,13 @@ export default { ...@@ -10,12 +10,13 @@ export default {
props: { props: {
status: { status: {
type: String, type: String,
required: true, required: false,
default: null,
}, },
}, },
computed: { computed: {
statusUi() { statusUi() {
return HEALTH_STATUS_UI[this.status.toLowerCase()]; return this.status ? HEALTH_STATUS_UI[this.status.toLowerCase()] : HEALTH_STATUS_UI.unknown;
}, },
}, },
}; };
...@@ -24,6 +25,6 @@ export default { ...@@ -24,6 +25,6 @@ export default {
<template> <template>
<gl-badge :variant="statusUi.variant"> <gl-badge :variant="statusUi.variant">
<gl-icon :name="statusUi.icon" /> <gl-icon :name="statusUi.icon" />
<span class="gl-ml-2 gl-font-weight-bold">{{ status }}</span> <span class="gl-ml-2 gl-font-weight-bold">{{ statusUi.text }}</span>
</gl-badge> </gl-badge>
</template> </template>
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
export const GEO_INFO_URL = helpPagePath('administration/geo/index.md'); export const GEO_INFO_URL = helpPagePath('administration/geo/index.md');
...@@ -31,22 +31,27 @@ export const HEALTH_STATUS_UI = { ...@@ -31,22 +31,27 @@ export const HEALTH_STATUS_UI = {
healthy: { healthy: {
icon: 'status_success', icon: 'status_success',
variant: 'success', variant: 'success',
text: s__('Geo|Healthy'),
}, },
unhealthy: { unhealthy: {
icon: 'status_failed', icon: 'status_failed',
variant: 'danger', variant: 'danger',
text: s__('Geo|Unhealthy'),
}, },
disabled: { disabled: {
icon: 'status_canceled', icon: 'status_canceled',
variant: 'neutral', variant: 'neutral',
text: s__('Geo|Disabled'),
}, },
unknown: { unknown: {
icon: 'status_notfound', icon: 'status_notfound',
variant: 'neutral', variant: 'neutral',
text: s__('Geo|Unknown'),
}, },
offline: { offline: {
icon: 'status_canceled', icon: 'status_canceled',
variant: 'neutral', variant: 'neutral',
text: s__('Geo|Offline'),
}, },
}; };
......
...@@ -42,6 +42,10 @@ describe('GeoNodePrimaryOtherInfo', () => { ...@@ -42,6 +42,10 @@ describe('GeoNodePrimaryOtherInfo', () => {
expect(findGlCard().exists()).toBe(true); expect(findGlCard().exists()).toBe(true);
}); });
it('renders the replication slot WAL section', () => {
expect(findReplicationSlotWAL().exists()).toBe(true);
});
it('renders the replicationSlots progress bar', () => { it('renders the replicationSlots progress bar', () => {
expect(findGeoNodeProgressBar().exists()).toBe(true); expect(findGeoNodeProgressBar().exists()).toBe(true);
}); });
...@@ -53,7 +57,6 @@ describe('GeoNodePrimaryOtherInfo', () => { ...@@ -53,7 +57,6 @@ describe('GeoNodePrimaryOtherInfo', () => {
}); });
it('renders the replicationSlotWAL section correctly', () => { it('renders the replicationSlotWAL section correctly', () => {
expect(findReplicationSlotWAL().exists()).toBe(true);
expect(findReplicationSlotWAL().text()).toBe( expect(findReplicationSlotWAL().text()).toBe(
numberToHumanSize(MOCK_NODES[0].replicationSlotsMaxRetainedWalBytes), numberToHumanSize(MOCK_NODES[0].replicationSlotsMaxRetainedWalBytes),
); );
...@@ -65,8 +68,8 @@ describe('GeoNodePrimaryOtherInfo', () => { ...@@ -65,8 +68,8 @@ describe('GeoNodePrimaryOtherInfo', () => {
createComponent({ node: MOCK_NODES[1] }); createComponent({ node: MOCK_NODES[1] });
}); });
it('does not render the replicationSlotWAL section', () => { it('renders Unknown', () => {
expect(findReplicationSlotWAL().exists()).toBe(false); expect(findReplicationSlotWAL().text()).toBe('Unknown');
}); });
}); });
}); });
......
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import GeoNodeSecondaryOtherInfo from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_secondary_other_info.vue'; import GeoNodeSecondaryOtherInfo from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_secondary_other_info.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'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
// Dates come from the backend in seconds, we mimic that here.
const MOCK_JUST_NOW = new Date().getTime() / 1000;
describe('GeoNodeSecondaryOtherInfo', () => { describe('GeoNodeSecondaryOtherInfo', () => {
let wrapper; let wrapper;
...@@ -17,6 +22,7 @@ describe('GeoNodeSecondaryOtherInfo', () => { ...@@ -17,6 +22,7 @@ describe('GeoNodeSecondaryOtherInfo', () => {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
stubs: { GlSprintf, TimeAgo },
}), }),
); );
}; };
...@@ -40,18 +46,12 @@ describe('GeoNodeSecondaryOtherInfo', () => { ...@@ -40,18 +46,12 @@ describe('GeoNodeSecondaryOtherInfo', () => {
expect(findDbReplicationLag().exists()).toBe(true); expect(findDbReplicationLag().exists()).toBe(true);
}); });
it('renders the last event correctly', () => { it('renders the last event', () => {
expect(findLastEvent().exists()).toBe(true); expect(findLastEvent().exists()).toBe(true);
expect(findLastEvent().props('time')).toBe(
new Date(MOCK_NODES[1].lastEventTimestamp * 1000).toString(),
);
}); });
it('renders the last cursor event correctly', () => { it('renders the last cursor event', () => {
expect(findLastCursorEvent().exists()).toBe(true); expect(findLastCursorEvent().exists()).toBe(true);
expect(findLastCursorEvent().props('time')).toBe(
new Date(MOCK_NODES[1].cursorLastEventTimestamp * 1000).toString(),
);
}); });
it('renders the storage shards', () => { it('renders the storage shards', () => {
...@@ -75,17 +75,45 @@ describe('GeoNodeSecondaryOtherInfo', () => { ...@@ -75,17 +75,45 @@ describe('GeoNodeSecondaryOtherInfo', () => {
}); });
describe.each` describe.each`
storageShardsMatch | text | hasErrorClass storageShardsMatch | text
${true} | ${'OK'} | ${false} ${true} | ${'OK'}
${false} | ${'Does not match the primary storage configuration'} | ${true} ${false} | ${'Does not match the primary storage configuration'}
`(`storage shards`, ({ storageShardsMatch, text, hasErrorClass }) => { ${null} | ${'Unknown'}
`(`storage shards`, ({ storageShardsMatch, text }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ node: { storageShardsMatch } }); createComponent({ node: { storageShardsMatch } });
}); });
it(`renders correctly when storageShardsMatch is ${storageShardsMatch}`, () => { it(`renders correctly when storageShardsMatch is ${storageShardsMatch}`, () => {
expect(findStorageShards().text()).toBe(text); expect(findStorageShards().text()).toBe(text);
expect(findStorageShards().classes('gl-text-red-500')).toBe(hasErrorClass); });
});
describe.each`
lastEvent | text
${{ lastEventId: null, lastEventTimestamp: null }} | ${'Unknown'}
${{ lastEventId: 1, lastEventTimestamp: MOCK_JUST_NOW }} | ${'1 (just now)'}
`(`last event`, ({ lastEvent, text }) => {
beforeEach(() => {
createComponent({ node: { ...lastEvent } });
});
it(`renders correctly when lastEventId is ${lastEvent.lastEventId}`, () => {
expect(findLastEvent().text().replace(/\s+/g, ' ')).toBe(text);
});
});
describe.each`
lastCursorEvent | text
${{ cursorLastEventId: null, cursorLastEventTimestamp: null }} | ${'Unknown'}
${{ cursorLastEventId: 1, cursorLastEventTimestamp: MOCK_JUST_NOW }} | ${'1 (just now)'}
`(`last cursor event`, ({ lastCursorEvent, text }) => {
beforeEach(() => {
createComponent({ node: { ...lastCursorEvent } });
});
it(`renders correctly when cursorLastEventId is ${lastCursorEvent.cursorLastEventId}`, () => {
expect(findLastCursorEvent().text().replace(/\s+/g, ' ')).toBe(text);
}); });
}); });
}); });
......
...@@ -43,10 +43,6 @@ describe('GeoNodeHeader', () => { ...@@ -43,10 +43,6 @@ describe('GeoNodeHeader', () => {
expect(findGeoNodeHealthStatus().exists()).toBe(true); expect(findGeoNodeHealthStatus().exists()).toBe(true);
}); });
it('renders the Geo Node Last Updated', () => {
expect(findGeoNodeLastUpdated().exists()).toBe(true);
});
it('renders the Geo Node Actions', () => { it('renders the Geo Node Actions', () => {
expect(findGeoNodeActions().exists()).toBe(true); expect(findGeoNodeActions().exists()).toBe(true);
}); });
...@@ -107,5 +103,29 @@ describe('GeoNodeHeader', () => { ...@@ -107,5 +103,29 @@ describe('GeoNodeHeader', () => {
}); });
}); });
}); });
describe('Last updated', () => {
describe('when lastSuccessfulStatusCheckTimestamp exists', () => {
beforeEach(() => {
createComponent({
node: { ...MOCK_NODES[1], lastSuccessfulStatusCheckTimestamp: new Date().getTime() },
});
});
it('renders', () => {
expect(findGeoNodeLastUpdated().exists()).toBe(true);
});
});
describe('when lastSuccessfulStatusCheckTimestamp does not exist', () => {
beforeEach(() => {
createComponent();
});
it('renders', () => {
expect(findGeoNodeLastUpdated().exists()).toBe(false);
});
});
});
}); });
}); });
...@@ -29,6 +29,7 @@ describe('GeoNodeHealthStatus', () => { ...@@ -29,6 +29,7 @@ describe('GeoNodeHealthStatus', () => {
describe.each` describe.each`
status | uiData status | uiData
${undefined} | ${HEALTH_STATUS_UI.unknown}
${'Healthy'} | ${HEALTH_STATUS_UI.healthy} ${'Healthy'} | ${HEALTH_STATUS_UI.healthy}
${'Unhealthy'} | ${HEALTH_STATUS_UI.unhealthy} ${'Unhealthy'} | ${HEALTH_STATUS_UI.unhealthy}
${'Disabled'} | ${HEALTH_STATUS_UI.disabled} ${'Disabled'} | ${HEALTH_STATUS_UI.disabled}
...@@ -48,8 +49,8 @@ describe('GeoNodeHealthStatus', () => { ...@@ -48,8 +49,8 @@ describe('GeoNodeHealthStatus', () => {
expect(findGeoStatusIcon().attributes('name')).toBe(uiData.icon); expect(findGeoStatusIcon().attributes('name')).toBe(uiData.icon);
}); });
it(`renders status text to ${status}`, () => { it(`renders status text to ${uiData.text}`, () => {
expect(findGeoStatusText().text()).toBe(status); expect(findGeoStatusText().text()).toBe(uiData.text);
}); });
}); });
}); });
......
...@@ -14538,6 +14538,9 @@ msgstr "" ...@@ -14538,6 +14538,9 @@ msgstr ""
msgid "Geo|%{component} synced" msgid "Geo|%{component} synced"
msgstr "" msgstr ""
msgid "Geo|%{eventId} (%{timeAgo})"
msgstr ""
msgid "Geo|%{itemTitle} checksum progress" msgid "Geo|%{itemTitle} checksum progress"
msgstr "" msgstr ""
...@@ -14616,6 +14619,9 @@ msgstr "" ...@@ -14616,6 +14619,9 @@ msgstr ""
msgid "Geo|Data type" msgid "Geo|Data type"
msgstr "" msgstr ""
msgid "Geo|Disabled"
msgstr ""
msgid "Geo|Discover GitLab Geo" msgid "Geo|Discover GitLab Geo"
msgstr "" msgstr ""
...@@ -14643,6 +14649,9 @@ msgstr "" ...@@ -14643,6 +14649,9 @@ msgstr ""
msgid "Geo|Go to the primary site" msgid "Geo|Go to the primary site"
msgstr "" msgstr ""
msgid "Geo|Healthy"
msgstr ""
msgid "Geo|If you want to make changes, you must visit the primary site." msgid "Geo|If you want to make changes, you must visit the primary site."
msgstr "" msgstr ""
...@@ -14706,6 +14715,9 @@ msgstr "" ...@@ -14706,6 +14715,9 @@ msgstr ""
msgid "Geo|Number of %{title}" msgid "Geo|Number of %{title}"
msgstr "" msgstr ""
msgid "Geo|Offline"
msgstr ""
msgid "Geo|Pending synchronization" msgid "Geo|Pending synchronization"
msgstr "" msgstr ""
...@@ -14868,6 +14880,12 @@ msgstr "" ...@@ -14868,6 +14880,12 @@ msgstr ""
msgid "Geo|Undefined" msgid "Geo|Undefined"
msgstr "" msgstr ""
msgid "Geo|Unhealthy"
msgstr ""
msgid "Geo|Unknown"
msgstr ""
msgid "Geo|Unknown state" msgid "Geo|Unknown state"
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