Commit d3370525 authored by Zack Cuddy's avatar Zack Cuddy

Better Geo Out of Date Errors

Currently when a Geo Node gets out of sync
the errors are scattered all around the card.

This is hard to follow for the user as the
location of the errors isn't always consistent.

This MR cleans up a lot of the current error UI
and consildates it.

The time since last synced is also now always
visible in the main section of the card.
parent b7d20bab
......@@ -39,16 +39,6 @@ export default {
type: [Object, String, Number],
required: true,
},
itemValueStale: {
type: Boolean,
required: false,
default: false,
},
itemValueStaleTooltip: {
type: String,
required: false,
default: '',
},
itemValueType: {
type: String,
required: true,
......@@ -104,10 +94,7 @@ export default {
:item-enabled="itemEnabled"
:item-title="itemTitle"
:item-value="itemValue"
:item-value-stale="itemValueStale"
:item-value-stale-tooltip="itemValueStaleTooltip"
:details-path="detailsPath"
:class="{ 'd-flex': itemValueStale }"
class="mt-1"
/>
<template v-if="isValueTypeCustom">
......
<script>
/* eslint-disable vue/no-side-effects-in-computed-properties */
import { GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
......@@ -42,28 +41,21 @@ export default {
required: true,
},
},
data() {
return {
showAdvanceItems: false,
errorMessage: '',
};
},
computed: {
hasError() {
if (!this.nodeDetails.healthy) {
this.errorMessage = this.nodeDetails.health;
}
return !this.nodeDetails.healthy;
},
hasVersionMismatch() {
if (
return (
this.nodeDetails.version !== this.nodeDetails.primaryVersion ||
this.nodeDetails.revision !== this.nodeDetails.primaryRevision
) {
this.errorMessage = s__('GeoNodes|GitLab version does not match the primary node version');
return true;
);
},
errorMessage() {
if (!this.nodeDetails.healthy) {
return this.nodeDetails.health;
} else if (this.hasVersionMismatch) {
return s__('GeoNodes|GitLab version does not match the primary node version');
}
return false;
return '';
},
},
};
......@@ -90,7 +82,7 @@ export default {
:node-details="nodeDetails"
:node-type-primary="node.primary"
/>
<div v-if="hasError || hasVersionMismatch">
<div v-if="errorMessage">
<p class="p-3 mb-0 bg-danger-100 text-danger-500">
{{ errorMessage }}
<gl-link :href="geoTroubleshootingHelpPath">{{
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import { GlIcon } from '@gitlab/ui';
import GeoNodeLastUpdated from './geo_node_last_updated.vue';
import { HEALTH_STATUS_ICON, HEALTH_STATUS_CLASS } from '../constants';
export default {
components: {
icon,
GlIcon,
GeoNodeLastUpdated,
},
props: {
status: {
type: String,
required: true,
},
statusCheckTimestamp: {
type: Number,
required: true,
},
},
computed: {
healthCssClass() {
......@@ -26,12 +32,15 @@ export default {
<template>
<div class="mt-2 detail-section-item">
<div class="text-secondary-700 node-detail-title">{{ s__('GeoNodes|Health status') }}</div>
<div
:class="healthCssClass"
class="rounded-pill d-inline-flex align-items-center px-2 py-1 mt-1"
>
<icon :size="16" :name="statusIconName" />
<span class="status-text ml-1 bold"> {{ status }} </span>
<div class="d-flex align-items-center">
<div
:class="healthCssClass"
class="rounded-pill d-inline-flex align-items-center px-2 py-1 my-1 mr-2"
>
<gl-icon :name="statusIconName" />
<strong class="status-text ml-1"> {{ status }} </strong>
</div>
<geo-node-last-updated :status-check-timestamp="statusCheckTimestamp" />
</div>
</div>
</template>
<script>
import { GlPopover, GlLink, GlIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import {
HELP_NODE_HEALTH_URL,
GEO_TROUBLESHOOTING_URL,
STATUS_DELAY_THRESHOLD_MS,
} from '../constants';
export default {
name: 'GeoNodeLastUpdated',
components: {
GlPopover,
GlLink,
GlIcon,
},
mixins: [timeAgoMixin],
props: {
statusCheckTimestamp: {
type: Number,
required: true,
},
},
computed: {
isSyncStale() {
const elapsedMilliseconds = Math.abs(this.statusCheckTimestamp - Date.now());
return elapsedMilliseconds > STATUS_DELAY_THRESHOLD_MS;
},
syncHelp() {
if (this.isSyncStale) {
return {
text: s__('GeoNodes|Consult Geo troubleshooting information'),
link: GEO_TROUBLESHOOTING_URL,
};
}
return {
text: s__('GeoNodes|Learn more about Geo node statuses'),
link: HELP_NODE_HEALTH_URL,
};
},
syncTimeAgo() {
const timeAgo = this.timeFormatted(this.statusCheckTimestamp);
return {
mainText: sprintf(s__('GeoNodes|Updated %{timeAgo}'), { timeAgo }),
popoverText: sprintf(s__("GeoNodes|Node's status was updated %{timeAgo}."), { timeAgo }),
};
},
},
};
</script>
<template>
<div class="d-flex align-items-center">
<span data-testid="nodeLastUpdateMainText" class="text-secondary-700">{{
syncTimeAgo.mainText
}}</span>
<gl-icon
ref="lastUpdated"
tabindex="0"
name="question"
class="text-primary-600 ml-1 cursor-pointer"
/>
<gl-popover :target="() => $refs.lastUpdated.$el" placement="top" triggers="hover focus">
<p>{{ syncTimeAgo.popoverText }}</p>
<gl-link class="mt-3 gl-font-size-small" :href="syncHelp.link" target="_blank">{{
syncHelp.text
}}</gl-link>
</gl-popover>
</div>
</template>
<script>
import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui';
import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
......@@ -14,7 +13,6 @@ export default {
GlSprintf,
GlLink,
StackedProgressBar,
Icon,
},
props: {
itemTitle: {
......@@ -27,16 +25,6 @@ export default {
validator: value =>
['totalCount', 'successCount', 'failureCount'].every(key => typeof value[key] === 'number'),
},
itemValueStale: {
type: Boolean,
required: false,
default: false,
},
itemValueStaleTooltip: {
type: String,
required: false,
default: '',
},
detailsPath: {
type: String,
required: false,
......@@ -56,7 +44,6 @@ export default {
<stacked-progress-bar
:id="`syncProgress-${itemTitle}`"
tabindex="0"
:css-class="itemValueStale ? 'flex-fill' : ''"
:hide-tooltips="true"
:unavailable-label="__('Nothing to synchronize')"
:success-count="itemValue.successCount"
......@@ -104,14 +91,5 @@ export default {
</div>
</section>
</gl-popover>
<icon
v-if="itemValueStale"
v-tooltip
:title="itemValueStaleTooltip"
:aria-label="itemValueStaleTooltip"
name="time-out"
class="ml-2 text-warning-500"
data-container="body"
/>
</div>
</template>
......@@ -100,7 +100,10 @@ export default {
{{ selectiveSyncronization }}
</span>
</div>
<geo-node-health-status :status="nodeHealthStatus" />
<geo-node-health-status
:status="nodeHealthStatus"
:status-check-timestamp="nodeDetails.statusCheckTimestamp"
/>
</div>
</div>
</template>
......@@ -4,8 +4,6 @@ import { numberToHumanSize } from '~/lib/utils/number_utils';
import { VALUE_TYPE } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
......@@ -15,7 +13,6 @@ export default {
SectionRevealButton,
GeoNodeDetailItem,
},
mixins: [DetailsSectionMixin],
props: {
node: {
type: Object,
......@@ -116,8 +113,6 @@ export default {
:item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType"
:item-value-stale="statusInfoStale"
:item-value-stale-tooltip="statusInfoStaleMessage"
/>
</div>
</div>
......
......@@ -4,8 +4,6 @@ import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { VALUE_TYPE, CUSTOM_TYPE } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
......@@ -14,7 +12,6 @@ export default {
SectionRevealButton,
GeoNodeDetailItem,
},
mixins: [DetailsSectionMixin],
props: {
node: {
type: Object,
......@@ -159,8 +156,6 @@ export default {
:item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType"
:item-value-stale="statusInfoStale"
:item-value-stale-tooltip="statusInfoStaleMessage"
:custom-type="nodeDetailItem.customType"
:event-type-log-status="nodeDetailItem.eventTypeLogStatus"
:details-path="nodeDetailItem.detailsPath"
......
......@@ -5,8 +5,6 @@ import { s__ } from '~/locale';
import { VALUE_TYPE, HELP_INFO_URL } from '../../constants';
import DetailsSectionMixin from '../../mixins/details_section_mixin';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
......@@ -19,7 +17,6 @@ export default {
GeoNodeDetailItem,
SectionRevealButton,
},
mixins: [DetailsSectionMixin],
props: {
nodeDetails: {
type: Object,
......@@ -135,8 +132,6 @@ export default {
:item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType"
:item-value-stale="statusInfoStale"
:item-value-stale-tooltip="statusInfoStaleMessage"
:success-label="nodeDetailItem.successLabel"
:neutral-label="nodeDetailItem.neutraLabel"
:failure-label="nodeDetailItem.failureLabel"
......
......@@ -36,10 +36,16 @@ export const TIME_DIFF = {
HOUR: 3600,
};
export const STATUS_DELAY_THRESHOLD_MS = 60000;
export const STATUS_DELAY_THRESHOLD_MS = 600000;
export const HELP_INFO_URL =
'https://docs.gitlab.com/ee/administration/geo/disaster_recovery/background_verification.html#repository-verification';
export const REPLICATION_HELP_URL =
'https://docs.gitlab.com/ee/administration/geo/replication/datatypes.html#limitations-on-replicationverification';
export const HELP_NODE_HEALTH_URL =
'https://docs.gitlab.com/ee/administration/geo/replication/troubleshooting.html#check-the-health-of-the-secondary-node';
export const GEO_TROUBLESHOOTING_URL =
'https://docs.gitlab.com/ee/administration/geo/replication/troubleshooting.html';
import { s__, sprintf } from '~/locale';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import { STATUS_DELAY_THRESHOLD_MS } from '../constants';
export default {
mixins: [timeAgoMixin],
computed: {
statusInfoStale() {
const elapsedMilliseconds = Math.abs(this.nodeDetails.statusCheckTimestamp - Date.now());
return elapsedMilliseconds > STATUS_DELAY_THRESHOLD_MS;
},
statusInfoStaleMessage() {
return sprintf(s__('GeoNodes|Data is out of date from %{timeago}'), {
timeago: this.timeFormatted(this.nodeDetails.statusCheckTimestamp),
});
},
},
};
......@@ -151,7 +151,7 @@ class GeoNodeStatus < ApplicationRecord
package_files_checksum_failed_count: 'Number of package files failed to checksum on primary'
}.freeze
EXPIRATION_IN_MINUTES = 5
EXPIRATION_IN_MINUTES = 10
HEALTHY_STATUS = 'Healthy'.freeze
UNHEALTHY_STATUS = 'Unhealthy'.freeze
......@@ -278,10 +278,6 @@ class GeoNodeStatus < ApplicationRecord
end
def health
if outdated?
return "Status has not been updated in the past #{EXPIRATION_IN_MINUTES} minutes"
end
status_message
end
......
---
title: Geo - Better Out of Date Errors
merge_request: 29800
author:
type: changed
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 1`] = `"<icon-stub name=\\"status_success\\" size=\\"16\\"></icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 1`] = `"<gl-icon-stub name=\\"status_success\\" size=\\"16\\"></gl-icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 2`] = `"<icon-stub name=\\"status_failed\\" size=\\"16\\"></icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 2`] = `"<gl-icon-stub name=\\"status_failed\\" size=\\"16\\"></gl-icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 3`] = `"<icon-stub name=\\"status_canceled\\" size=\\"16\\"></icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 3`] = `"<gl-icon-stub name=\\"status_canceled\\" size=\\"16\\"></gl-icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 4`] = `"<icon-stub name=\\"status_notfound\\" size=\\"16\\"></icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 4`] = `"<gl-icon-stub name=\\"status_notfound\\" size=\\"16\\"></gl-icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 5`] = `"<icon-stub name=\\"status_canceled\\" size=\\"16\\"></icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders Icon correctly 5`] = `"<gl-icon-stub name=\\"status_canceled\\" size=\\"16\\"></gl-icon-stub>"`;
exports[`GeoNodeHealthStatusComponent computed properties renders StatusPill correctly 1`] = `
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 mt-1 text-success-600 bg-success-100\\">
<icon-stub name=\\"status_success\\" size=\\"16\\"></icon-stub> <span class=\\"status-text ml-1 bold\\"> Healthy </span>
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 my-1 mr-2 text-success-600 bg-success-100\\">
<gl-icon-stub name=\\"status_success\\" size=\\"16\\"></gl-icon-stub> <strong class=\\"status-text ml-1\\"> Healthy </strong>
</div>"
`;
exports[`GeoNodeHealthStatusComponent computed properties renders StatusPill correctly 2`] = `
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 mt-1 text-danger-600 bg-danger-100\\">
<icon-stub name=\\"status_failed\\" size=\\"16\\"></icon-stub> <span class=\\"status-text ml-1 bold\\"> Unhealthy </span>
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 my-1 mr-2 text-danger-600 bg-danger-100\\">
<gl-icon-stub name=\\"status_failed\\" size=\\"16\\"></gl-icon-stub> <strong class=\\"status-text ml-1\\"> Unhealthy </strong>
</div>"
`;
exports[`GeoNodeHealthStatusComponent computed properties renders StatusPill correctly 3`] = `
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 mt-1 text-secondary-800 bg-secondary-100\\">
<icon-stub name=\\"status_canceled\\" size=\\"16\\"></icon-stub> <span class=\\"status-text ml-1 bold\\"> Disabled </span>
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 my-1 mr-2 text-secondary-800 bg-secondary-100\\">
<gl-icon-stub name=\\"status_canceled\\" size=\\"16\\"></gl-icon-stub> <strong class=\\"status-text ml-1\\"> Disabled </strong>
</div>"
`;
exports[`GeoNodeHealthStatusComponent computed properties renders StatusPill correctly 4`] = `
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 mt-1 text-secondary-800 bg-secondary-100\\">
<icon-stub name=\\"status_notfound\\" size=\\"16\\"></icon-stub> <span class=\\"status-text ml-1 bold\\"> Unknown </span>
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 my-1 mr-2 text-secondary-800 bg-secondary-100\\">
<gl-icon-stub name=\\"status_notfound\\" size=\\"16\\"></gl-icon-stub> <strong class=\\"status-text ml-1\\"> Unknown </strong>
</div>"
`;
exports[`GeoNodeHealthStatusComponent computed properties renders StatusPill correctly 5`] = `
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 mt-1 text-secondary-800 bg-secondary-100\\">
<icon-stub name=\\"status_canceled\\" size=\\"16\\"></icon-stub> <span class=\\"status-text ml-1 bold\\"> Offline </span>
"<div class=\\"rounded-pill d-inline-flex align-items-center px-2 py-1 my-1 mr-2 text-secondary-800 bg-secondary-100\\">
<gl-icon-stub name=\\"status_canceled\\" size=\\"16\\"></gl-icon-stub> <strong class=\\"status-text ml-1\\"> Offline </strong>
</div>"
`;
......@@ -33,80 +33,81 @@ describe('GeoNodeDetailsComponent', () => {
wrapper.destroy();
});
describe('data', () => {
it('returns default data props', () => {
expect(wrapper.vm.showAdvanceItems).toBeFalsy();
expect(wrapper.vm.errorMessage).toBe('');
const findErrorSection = () => wrapper.find('.bg-danger-100');
const findTroubleshootingLink = () => findErrorSection().find(GlLink);
describe('template', () => {
it('renders container elements correctly', () => {
expect(wrapper.classes('card-body')).toBe(true);
});
});
describe('computed', () => {
describe('hasError', () => {
beforeEach(() => {
const nodeDetails = Object.assign({}, mockNodeDetails, {
health: 'Something went wrong.',
healthy: false,
describe('when unhealthy', () => {
describe('with errorMessage', () => {
beforeEach(() => {
createComponent({
nodeDetails: {
...defaultProps.nodeDetails,
healthy: false,
health: 'This is an error',
},
});
});
it('renders error message section', () => {
expect(findErrorSection().text()).toContain('This is an error');
});
createComponent({ nodeDetails });
it('renders troubleshooting URL within error message section', () => {
expect(findTroubleshootingLink().attributes('href')).toBe('/foo/bar');
});
});
it('returns boolean value representing if node has any errors', () => {
// With altered mock data for Unhealthy status
expect(wrapper.vm.errorMessage).toBe('Something went wrong.');
expect(wrapper.vm.hasError).toBeTruthy();
describe('without error message', () => {
beforeEach(() => {
createComponent({
nodeDetails: {
...defaultProps.nodeDetails,
healthy: false,
health: '',
},
});
});
// With default mock data
expect(defaultProps.hasError).toBeFalsy();
it('does not render error message section', () => {
expect(findErrorSection().exists()).toBeFalsy();
});
});
});
describe('hasVersionMismatch', () => {
describe('when healthy', () => {
beforeEach(() => {
const nodeDetails = Object.assign({}, mockNodeDetails, {
primaryVersion: '10.3.0-pre',
primaryRevision: 'b93c51850b',
});
createComponent({ nodeDetails });
createComponent();
});
it('returns boolean value representing if node has version mismatch', () => {
// With altered mock data for version mismatch
expect(wrapper.vm.errorMessage).toBe(
'GitLab version does not match the primary node version',
);
expect(wrapper.vm.hasVersionMismatch).toBeTruthy();
// With default mock data
expect(defaultProps.hasVersionMismatch).toBeFalsy();
it('does not render error message section', () => {
expect(findErrorSection().exists()).toBeFalsy();
});
});
});
describe('template', () => {
it('renders container elements correctly', () => {
expect(wrapper.vm.$el.classList.contains('card-body')).toBe(true);
});
describe('with error', () => {
describe('when version mismatched', () => {
beforeEach(() => {
createComponent({
errorMessage: 'Foobar',
nodeDetails: {
...defaultProps.nodeDetails,
healthy: false,
primaryVersion: '10.3.0-pre',
primaryRevision: 'b93c51850b',
},
});
});
it('renders error message section', () => {
expect(findErrorSection().text()).toContain(
'GitLab version does not match the primary node version',
);
});
it('renders troubleshooting URL within error message section', () => {
expect(
wrapper
.find('.bg-danger-100')
.find(GlLink)
.attributes('href'),
).toBe('/foo/bar');
expect(findTroubleshootingLink().attributes('href')).toBe('/foo/bar');
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import Icon from '~/vue_shared/components/icon.vue';
import { GlIcon } from '@gitlab/ui';
import geoNodeHealthStatusComponent from 'ee/geo_nodes/components/geo_node_health_status.vue';
import { HEALTH_STATUS_ICON, HEALTH_STATUS_CLASS } from 'ee/geo_nodes/constants';
import { mockNodeDetails } from '../mock_data';
......@@ -9,6 +9,7 @@ describe('GeoNodeHealthStatusComponent', () => {
const defaultProps = {
status: mockNodeDetails.health,
statusCheckTimestamp: mockNodeDetails.statusCheckTimestamp,
};
const createComponent = (props = {}) => {
......@@ -26,7 +27,7 @@ describe('GeoNodeHealthStatusComponent', () => {
});
const findStatusPill = () => wrapper.find('.rounded-pill');
const findStatusIcon = () => findStatusPill().find(Icon);
const findStatusIcon = () => findStatusPill().find(GlIcon);
describe.each`
status | healthCssClass | statusIconName
......
import { shallowMount } from '@vue/test-utils';
import { GlPopover, GlLink, GlIcon } from '@gitlab/ui';
import GeoNodeLastUpdated from 'ee/geo_nodes/components/geo_node_last_updated.vue';
import {
HELP_NODE_HEALTH_URL,
GEO_TROUBLESHOOTING_URL,
STATUS_DELAY_THRESHOLD_MS,
} from 'ee/geo_nodes/constants';
describe('GeoNodeLastUpdated', () => {
let wrapper;
const staleStatusTime = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS).getTime();
const nonStaleStatusTime = new Date().getTime();
const defaultProps = {
statusCheckTimestamp: staleStatusTime,
};
const createComponent = (props = {}) => {
wrapper = shallowMount(GeoNodeLastUpdated, {
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findMainText = () => wrapper.find('[data-testid="nodeLastUpdateMainText"]');
const findGlIcon = () => wrapper.find(GlIcon);
const findGlPopover = () => wrapper.find(GlPopover);
const findPopoverText = () => findGlPopover().find('p');
const findPopoverLink = () => findGlPopover().find(GlLink);
describe('template', () => {
beforeEach(() => {
createComponent();
});
describe('Main Text', () => {
it('renders always', () => {
expect(findMainText().exists()).toBeTruthy();
});
it('should properly display time ago', () => {
expect(findMainText().text()).toBe('Updated 10 minutes ago');
});
});
describe('Question Icon', () => {
it('renders always', () => {
expect(findGlIcon().exists()).toBeTruthy();
});
it('sets to question icon', () => {
expect(findGlIcon().attributes('name')).toBe('question');
});
});
it('renders popover always', () => {
expect(findGlPopover().exists()).toBeTruthy();
});
describe('Popover Text', () => {
it('renders always', () => {
expect(findPopoverText().exists()).toBeTruthy();
});
it('should properly display time ago', () => {
expect(findPopoverText().text()).toBe("Node's status was updated 10 minutes ago.");
});
});
describe('Popover Link', () => {
describe('when sync is stale', () => {
it('text should mention troubleshooting', () => {
expect(findPopoverLink().text()).toBe('Consult Geo troubleshooting information');
});
it('link should be to GEO_TROUBLESHOOTING_URL', () => {
expect(findPopoverLink().attributes('href')).toBe(GEO_TROUBLESHOOTING_URL);
});
});
describe('when sync is not stale', () => {
beforeEach(() => {
createComponent({ statusCheckTimestamp: nonStaleStatusTime });
});
it('text should not mention troubleshooting', () => {
expect(findPopoverLink().text()).toBe('Learn more about Geo node statuses');
});
it('link should be to HELP_NODE_HEALTH_URL', () => {
expect(findPopoverLink().attributes('href')).toBe(HELP_NODE_HEALTH_URL);
});
});
});
it('renders popover link always', () => {
expect(findPopoverLink().exists()).toBeTruthy();
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlPopover } from '@gitlab/ui';
import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue';
import Icon from '~/vue_shared/components/icon.vue';
import GeoNodeSyncProgress from 'ee/geo_nodes/components/geo_node_sync_progress.vue';
......@@ -34,7 +33,6 @@ describe('GeoNodeSyncProgress', () => {
const findStackedProgressBar = () => wrapper.find(StackedProgressBar);
const findGlPopover = () => wrapper.find(GlPopover);
const findCounts = () => findGlPopover().findAll('div');
const findStaleIcon = () => wrapper.find(Icon);
describe('template', () => {
beforeEach(() => {
......@@ -56,25 +54,6 @@ describe('GeoNodeSyncProgress', () => {
});
});
});
describe('when itemValueStale is false', () => {
it('does not render StaleIcon always', () => {
expect(findStaleIcon().exists()).toBeFalsy();
});
});
describe('when itemValueStale is true', () => {
beforeEach(() => {
createComponent({
itemValueStale: true,
itemValueStaleTooltip: 'Stale',
});
});
it('does render StaleIcon always', () => {
expect(findStaleIcon().exists()).toBeTruthy();
});
});
});
describe('computed', () => {
......
import Vue from 'vue';
import DetailsSectionMixin from 'ee/geo_nodes/mixins/details_section_mixin';
import { STATUS_DELAY_THRESHOLD_MS } from 'ee/geo_nodes/constants';
import mountComponent from 'helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../mock_data';
const createComponent = (nodeDetails = mockNodeDetails) => {
const Component = Vue.extend({
mixins: [DetailsSectionMixin],
data() {
return { nodeDetails };
},
render(h) {
return h('div');
},
});
return mountComponent(Component);
};
describe('DetailsSectionMixin', () => {
let vm;
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('statusInfoStale', () => {
it('returns true when `nodeDetails.statusCheckTimestamp` is past the value of STATUS_DELAY_THRESHOLD_MS', () => {
// Move statusCheckTimestamp to 2 minutes in the past
const statusCheckTimestamp = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS * 2).getTime();
vm = createComponent(Object.assign({}, mockNodeDetails, { statusCheckTimestamp }));
expect(vm.statusInfoStale).toBe(true);
});
it('returns false when `nodeDetails.statusCheckTimestamp` is under the value of STATUS_DELAY_THRESHOLD_MS', () => {
// Move statusCheckTimestamp to 30 seconds in the past
const statusCheckTimestamp = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS / 2).getTime();
vm = createComponent(Object.assign({}, mockNodeDetails, { statusCheckTimestamp }));
expect(vm.statusInfoStale).toBe(false);
});
});
describe('statusInfoStaleMessage', () => {
it('returns stale information message containing the duration elapsed', () => {
// Move statusCheckTimestamp to 1 minute in the past
const statusCheckTimestamp = new Date(Date.now() - STATUS_DELAY_THRESHOLD_MS).getTime();
vm = createComponent(Object.assign({}, mockNodeDetails, { statusCheckTimestamp }));
expect(vm.statusInfoStaleMessage).toBe('Data is out of date from 1 minute ago');
});
});
});
});
......@@ -70,7 +70,7 @@ describe GeoNodeStatus, :geo, :geo_fdw do
context 'takes outdated? into consideration' do
it 'return false' do
subject.status_message = GeoNodeStatus::HEALTHY_STATUS
subject.updated_at = 10.minutes.ago
subject.updated_at = 11.minutes.ago
expect(subject.healthy?).to be false
end
......@@ -86,7 +86,7 @@ describe GeoNodeStatus, :geo, :geo_fdw do
describe '#outdated?' do
it 'return true' do
subject.updated_at = 10.minutes.ago
subject.updated_at = 11.minutes.ago
expect(subject.outdated?).to be true
end
......@@ -107,20 +107,11 @@ describe GeoNodeStatus, :geo, :geo_fdw do
end
describe '#health' do
context 'takes outdated? into consideration' do
it 'returns expiration error' do
subject.status_message = GeoNodeStatus::HEALTHY_STATUS
subject.updated_at = 10.minutes.ago
expect(subject.health).to eq "Status has not been updated in the past #{described_class::EXPIRATION_IN_MINUTES} minutes"
end
it 'returns status message' do
subject.status_message = 'something went wrong'
subject.updated_at = 11.minutes.ago
it 'returns original message' do
subject.status_message = 'something went wrong'
subject.updated_at = 1.minute.ago
expect(subject.health).to eq 'something went wrong'
end
expect(subject.health).to eq 'something went wrong'
end
end
......
......@@ -9521,10 +9521,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
msgid "GeoNodes|Container repositories"
msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
msgid "GeoNodes|Data is out of date from %{timeago}"
msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
......@@ -9566,6 +9566,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
msgid "GeoNodes|Learn more about Geo node statuses"
msgstr ""
msgid "GeoNodes|Loading nodes"
msgstr ""
......@@ -9581,6 +9584,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
msgid "GeoNodes|Node's status was updated %{timeAgo}."
msgstr ""
msgid "GeoNodes|Not checksummed"
msgstr ""
......@@ -9641,6 +9647,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
msgid "GeoNodes|Updated %{timeAgo}"
msgstr ""
msgid "GeoNodes|Used slots"
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