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