Commit eca8d9af authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '211862-better-geo-out-of-date' into 'master'

Better Geo Out of Date Errors

Closes #211862

See merge request gitlab-org/gitlab!29800
parents b404aba3 d3370525
......@@ -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="d-flex align-items-center">
<div
:class="healthCssClass"
class="rounded-pill d-inline-flex align-items-center px-2 py-1 mt-1"
class="rounded-pill d-inline-flex align-items-center px-2 py-1 my-1 mr-2"
>
<icon :size="16" :name="statusIconName" />
<span class="status-text ml-1 bold"> {{ status }} </span>
<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', () => {
describe('when unhealthy', () => {
describe('with errorMessage', () => {
beforeEach(() => {
const nodeDetails = Object.assign({}, mockNodeDetails, {
health: 'Something went wrong.',
createComponent({
nodeDetails: {
...defaultProps.nodeDetails,
healthy: false,
health: 'This is an error',
},
});
createComponent({ nodeDetails });
});
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();
it('renders error message section', () => {
expect(findErrorSection().text()).toContain('This is an error');
});
// With default mock data
expect(defaultProps.hasError).toBeFalsy();
it('renders troubleshooting URL within error message section', () => {
expect(findTroubleshootingLink().attributes('href')).toBe('/foo/bar');
});
});
describe('hasVersionMismatch', () => {
describe('without error message', () => {
beforeEach(() => {
const nodeDetails = Object.assign({}, mockNodeDetails, {
primaryVersion: '10.3.0-pre',
primaryRevision: 'b93c51850b',
createComponent({
nodeDetails: {
...defaultProps.nodeDetails,
healthy: false,
health: '',
},
});
createComponent({ nodeDetails });
});
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('when healthy', () => {
beforeEach(() => {
createComponent();
});
it('does not render error message section', () => {
expect(findErrorSection().exists()).toBeFalsy();
});
});
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,22 +107,13 @@ 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 original message' do
it 'returns status message' do
subject.status_message = 'something went wrong'
subject.updated_at = 1.minute.ago
subject.updated_at = 11.minutes.ago
expect(subject.health).to eq 'something went wrong'
end
end
end
describe '#projects_count' do
it 'counts the number of projects' do
......
......@@ -9518,10 +9518,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"
......@@ -9563,6 +9563,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 ""
......@@ -9578,6 +9581,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
msgid "GeoNodes|Node's status was updated %{timeAgo}."
msgstr ""
msgid "GeoNodes|Not checksummed"
msgstr ""
......@@ -9638,6 +9644,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