Commit a808713a authored by Phil Hughes's avatar Phil Hughes

Merge branch '4909-show-status-data-timeout-info' into 'master'

Show status information stale icon in Geo admin dashboard

Closes #4909

See merge request gitlab-org/gitlab-ee!5653
parents 99ca7ab4 e9f660d8
<script> <script>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import popover from '~/vue_shared/directives/popover'; import popover from '~/vue_shared/directives/popover';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue'; import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue';
...@@ -18,6 +19,7 @@ ...@@ -18,6 +19,7 @@
}, },
directives: { directives: {
popover, popover,
tooltip,
}, },
props: { props: {
itemTitle: { itemTitle: {
...@@ -33,6 +35,16 @@ ...@@ -33,6 +35,16 @@
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: '',
},
successLabel: { successLabel: {
type: String, type: String,
required: false, required: false,
...@@ -132,8 +144,10 @@ ...@@ -132,8 +144,10 @@
<div <div
v-if="isValueTypeGraph" v-if="isValueTypeGraph"
class="node-detail-value" class="node-detail-value"
:class="{ 'd-flex': itemValueStale }"
> >
<stacked-progress-bar <stacked-progress-bar
:css-class="itemValueStale ? 'flex-fill' : ''"
:success-label="successLabel" :success-label="successLabel"
:failure-label="failureLabel" :failure-label="failureLabel"
:neutral-label="neutralLabel" :neutral-label="neutralLabel"
...@@ -141,6 +155,14 @@ ...@@ -141,6 +155,14 @@
:failure-count="itemValue.failureCount" :failure-count="itemValue.failureCount"
:total-count="itemValue.totalCount" :total-count="itemValue.totalCount"
/> />
<icon
v-tooltip
v-show="itemValueStale"
name="time-out"
css-classes="prepend-left-10 detail-value-stale-icon"
data-container="body"
:title="itemValueStaleTooltip"
/>
</div> </div>
<template v-if="isValueTypeCustom"> <template v-if="isValueTypeCustom">
<geo-node-sync-settings <geo-node-sync-settings
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
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';
...@@ -13,6 +15,9 @@ ...@@ -13,6 +15,9 @@
SectionRevealButton, SectionRevealButton,
GeoNodeDetailItem, GeoNodeDetailItem,
}, },
mixins: [
DetailsSectionMixin,
],
props: { props: {
nodeDetails: { nodeDetails: {
type: Object, type: Object,
...@@ -98,14 +103,12 @@ ...@@ -98,14 +103,12 @@
class="col-md-6 prepend-left-15 prepend-top-10 section-items-container" class="col-md-6 prepend-left-15 prepend-top-10 section-items-container"
> >
<geo-node-detail-item <geo-node-detail-item
v-for="(nodeDetailItem, index) in nodeDetailItems" :item-title="s__('GeoNodes|Storage config')"
:key="index" :item-value="storageShardsStatus"
:css-class="nodeDetailItem.cssClass" :item-value-type="$options.valueType.PLAIN"
:item-title="nodeDetailItem.itemTitle" :item-value-stale="statusInfoStale"
:item-value="nodeDetailItem.itemValue" :item-value-stale-tooltip="statusInfoStaleMessage"
:item-value-type="nodeDetailItem.itemValueType" :css-class="storageShardsCssClass"
:success-label="nodeDetailItem.successLabel"
:neutral-label="nodeDetailItem.neutraLabel"
/> />
</div> </div>
</div> </div>
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
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';
...@@ -12,6 +14,9 @@ ...@@ -12,6 +14,9 @@
SectionRevealButton, SectionRevealButton,
GeoNodeDetailItem, GeoNodeDetailItem,
}, },
mixins: [
DetailsSectionMixin,
],
props: { props: {
nodeDetails: { nodeDetails: {
type: Object, type: Object,
...@@ -135,6 +140,8 @@ ...@@ -135,6 +140,8 @@
: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"
/> />
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
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';
...@@ -11,6 +13,9 @@ ...@@ -11,6 +13,9 @@
GeoNodeDetailItem, GeoNodeDetailItem,
SectionRevealButton, SectionRevealButton,
}, },
mixins: [
DetailsSectionMixin,
],
props: { props: {
nodeDetails: { nodeDetails: {
type: Object, type: Object,
...@@ -122,6 +127,8 @@ ...@@ -122,6 +127,8 @@
: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"
......
...@@ -28,4 +28,6 @@ export const TIME_DIFF = { ...@@ -28,4 +28,6 @@ export const TIME_DIFF = {
HOUR: 3600, HOUR: 3600,
}; };
export const STATUS_DELAY_THRESHOLD_MS = 60000;
export const HELP_INFO_URL = 'https://docs.gitlab.com/ee/administration/geo/disaster_recovery/background_verification.html#repository-verification'; export const HELP_INFO_URL = 'https://docs.gitlab.com/ee/administration/geo/disaster_recovery/background_verification.html#repository-verification';
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.timeFormated(
this.nodeDetails.statusCheckTimestamp,
),
});
},
},
};
...@@ -68,6 +68,7 @@ export default class GeoNodesStore { ...@@ -68,6 +68,7 @@ export default class GeoNodesStore {
revision: rawNodeDetails.revision, revision: rawNodeDetails.revision,
primaryVersion: rawNodeDetails.primaryVersion, primaryVersion: rawNodeDetails.primaryVersion,
primaryRevision: rawNodeDetails.primaryRevision, primaryRevision: rawNodeDetails.primaryRevision,
statusCheckTimestamp: rawNodeDetails.last_successful_status_check_timestamp * 1000,
replicationSlotWAL: rawNodeDetails.replication_slots_max_retained_wal_bytes, replicationSlotWAL: rawNodeDetails.replication_slots_max_retained_wal_bytes,
missingOAuthApplication: rawNodeDetails.missing_oauth_application || false, missingOAuthApplication: rawNodeDetails.missing_oauth_application || false,
syncStatusUnavailable: rawNodeDetails.sync_status_unavailable || false, syncStatusUnavailable: rawNodeDetails.sync_status_unavailable || false,
......
...@@ -115,6 +115,10 @@ ...@@ -115,6 +115,10 @@
margin-top: 4px; margin-top: 4px;
} }
.detail-value-stale-icon {
color: $gl-warning;
}
.node-detail-value-bold { .node-detail-value-bold {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
......
---
title: Show status information stale icon in Geo admin dashboard
merge_request: 5653
author:
type: added
...@@ -56,6 +56,22 @@ describe('GeoNodeDetailItemComponent', () => { ...@@ -56,6 +56,22 @@ describe('GeoNodeDetailItemComponent', () => {
vm.$destroy(); vm.$destroy();
}); });
it('renders stale information status icon when `itemValueStale` prop is true', () => {
const itemValueStaleTooltip = 'Data is out of date from 8 hours ago';
const vm = createComponent({
itemValueType: VALUE_TYPE.GRAPH,
itemValue: { successCount: 5, failureCount: 3, totalCount: 10 },
itemValueStale: true,
itemValueStaleTooltip,
});
const iconEl = vm.$el.querySelector('.detail-value-stale-icon');
expect(iconEl).not.toBeNull();
expect(iconEl.dataset.originalTitle).toBe(itemValueStaleTooltip);
expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('time-out');
vm.$destroy();
});
it('renders sync settings item value', () => { it('renders sync settings item value', () => {
const vm = createComponent({ const vm = createComponent({
itemValueType: VALUE_TYPE.CUSTOM, itemValueType: VALUE_TYPE.CUSTOM,
......
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 'spec/helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../mock_data';
const createComponent = (nodeDetails = mockNodeDetails) => {
const Component = Vue.extend({
template: '<div></div>',
mixins: [DetailsSectionMixin],
data() {
return { nodeDetails };
},
});
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 about a minute ago');
});
});
});
});
...@@ -148,6 +148,7 @@ export const mockNodeDetails = { ...@@ -148,6 +148,7 @@ export const mockNodeDetails = {
revision: 'b93c51849b', revision: 'b93c51849b',
primaryVersion: '10.4.0-pre', primaryVersion: '10.4.0-pre',
primaryRevision: 'b93c51849b', primaryRevision: 'b93c51849b',
statusCheckTimestamp: 1515142330,
replicationSlotWAL: 502658737, replicationSlotWAL: 502658737,
missingOAuthApplication: false, missingOAuthApplication: false,
storageShardsMatch: false, storageShardsMatch: false,
......
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