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 { ...@@ -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
......
...@@ -9518,10 +9518,10 @@ msgstr "" ...@@ -9518,10 +9518,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"
...@@ -9563,6 +9563,9 @@ msgstr "" ...@@ -9563,6 +9563,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 ""
...@@ -9578,6 +9581,9 @@ msgstr "" ...@@ -9578,6 +9581,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 ""
...@@ -9638,6 +9644,9 @@ msgstr "" ...@@ -9638,6 +9644,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