Commit 9042365e authored by Zack Cuddy's avatar Zack Cuddy Committed by Mike Greiling

Geo Node Status 2.0 - Replication Summary

parent e4757344
<script>
import { GlIcon, GlPopover, GlLink } from '@gitlab/ui';
import { REPLICATION_STATUS_UI, REPLICATION_PAUSE_URL } from 'ee/geo_nodes_beta/constants';
import { __, s__ } from '~/locale';
export default {
name: 'GeoNodeReplicationStatus',
i18n: {
pauseHelpText: s__('Geo|Geo nodes are paused using a command run on the node'),
learnMore: __('Learn more'),
},
components: {
GlIcon,
GlPopover,
GlLink,
},
props: {
node: {
type: Object,
required: true,
},
},
computed: {
replicationStatusUi() {
return this.node.enabled ? REPLICATION_STATUS_UI.enabled : REPLICATION_STATUS_UI.disabled;
},
},
REPLICATION_PAUSE_URL,
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center">
<gl-icon
:name="replicationStatusUi.icon"
:class="replicationStatusUi.color"
data-testid="replication-status-icon"
/>
<span
class="gl-font-weight-bold"
:class="replicationStatusUi.color"
data-testid="replication-status-text"
>{{ replicationStatusUi.text }}</span
>
<gl-icon
ref="replicationStatus"
name="question"
class="gl-text-blue-500 gl-cursor-pointer gl-ml-2"
/>
<gl-popover :target="() => $refs.replicationStatus.$el" placement="top" triggers="hover focus">
<p class="gl-font-base">
{{ $options.i18n.pauseHelpText }}
</p>
<gl-link :href="$options.REPLICATION_PAUSE_URL" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
</gl-popover>
</div>
</template>
<script>
import { GlCard, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import GeoNodeReplicationStatus from './geo_node_replication_status.vue';
import GeoNodeSyncSettings from './geo_node_sync_settings.vue';
export default {
name: 'GeoNodeReplicationSummary',
......@@ -14,6 +16,8 @@ export default {
components: {
GlCard,
GlButton,
GeoNodeReplicationStatus,
GeoNodeSyncSettings,
},
props: {
node: {
......@@ -38,10 +42,12 @@ export default {
>
</template>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span data-testid="replication-status">{{ $options.i18n.replicationStatus }}</span>
<span>{{ $options.i18n.replicationStatus }}</span>
<geo-node-replication-status class="gl-mt-3" :node="node" />
</div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span data-testid="sync-settings">{{ $options.i18n.syncSettings }}</span>
<span>{{ $options.i18n.syncSettings }}</span>
<geo-node-sync-settings class="gl-mt-2" :node="node" />
</div>
<span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span>
</gl-card>
......
<script>
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { sprintf, __, s__ } from '~/locale';
export default {
name: 'GeoNodeSyncSettings',
i18n: {
full: __('Full'),
groups: __('groups'),
syncLabel: s__('Geo|Selective (%{syncLabel})'),
pendingEvents: s__('Geo|%{timeAgoStr} (%{pendingEvents} events)'),
},
props: {
node: {
type: Object,
required: true,
},
},
computed: {
syncType() {
if (this.node.selectiveSyncType === null || this.node.selectiveSyncType === '') {
return this.$options.i18n.full;
}
// Renaming namespaces to groups in the UI for Geo Selective Sync
const syncLabel =
this.node.selectiveSyncType === 'namespaces'
? this.$options.i18n.groups
: this.node.selectiveSyncType;
return sprintf(this.$options.i18n.syncLabel, { syncLabel });
},
eventTimestampEmpty() {
return !this.node.lastEventTimestamp || !this.node.cursorLastEventTimestamp;
},
syncLagInSeconds() {
return this.node.cursorLastEventTimestamp - this.node.lastEventTimestamp;
},
syncStatusEventInfo() {
const timeAgoStr = timeIntervalInWords(this.syncLagInSeconds);
const pendingEvents = this.node.lastEventId - this.node.cursorLastEventId;
return sprintf(this.$options.i18n.pendingEvents, {
timeAgoStr,
pendingEvents,
});
},
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center">
<span class="gl-font-weight-bold" data-testid="sync-type">{{ syncType }}</span>
<span
v-if="!eventTimestampEmpty"
class="gl-ml-3 gl-text-gray-500 gl-font-sm"
data-testid="sync-status-event-info"
>
{{ syncStatusEventInfo }}
</span>
</div>
</template>
import { helpPagePath } from '~/helpers/help_page_helper';
import { __ } from '~/locale';
export const GEO_INFO_URL = helpPagePath('administration/geo/index.md');
......@@ -17,6 +18,10 @@ export const HELP_INFO_URL = helpPagePath(
{ anchor: 'repository-verification' },
);
export const REPLICATION_PAUSE_URL = helpPagePath('administration/geo/index.html', {
anchor: 'pausing-and-resuming-replication',
});
export const HEALTH_STATUS_UI = {
healthy: {
icon: 'status_success',
......@@ -40,4 +45,17 @@ export const HEALTH_STATUS_UI = {
},
};
export const REPLICATION_STATUS_UI = {
enabled: {
icon: 'play',
color: 'gl-text-green-600',
text: __('Enabled'),
},
disabled: {
icon: 'pause',
color: 'gl-text-orange-600',
text: __('Paused'),
},
};
export const STATUS_DELAY_THRESHOLD_MS = 600000;
import { GlPopover, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import GeoNodeReplicationStatus from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status.vue';
import { REPLICATION_STATUS_UI, REPLICATION_PAUSE_URL } from 'ee/geo_nodes_beta/constants';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeReplicationStatus', () => {
let wrapper;
const defaultProps = {
node: MOCK_NODES[1],
};
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(GeoNodeReplicationStatus, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findReplicationStatusIcon = () => wrapper.findByTestId('replication-status-icon');
const findReplicationStatusText = () => wrapper.findByTestId('replication-status-text');
const findQuestionIcon = () => wrapper.find({ ref: 'replicationStatus' });
const findGlPopover = () => wrapper.findComponent(GlPopover);
const findGlPopoverLink = () => findGlPopover().findComponent(GlLink);
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the replication status icon', () => {
expect(findReplicationStatusIcon().exists()).toBe(true);
});
it('renders the replication status text', () => {
expect(findReplicationStatusText().exists()).toBe(true);
});
it('renders the question icon correctly', () => {
expect(findQuestionIcon().exists()).toBe(true);
expect(findQuestionIcon().attributes('name')).toBe('question');
});
it('renders the GlPopover always', () => {
expect(findGlPopover().exists()).toBe(true);
});
it('renders the popover link correctly', () => {
expect(findGlPopoverLink().exists()).toBe(true);
expect(findGlPopoverLink().attributes('href')).toBe(REPLICATION_PAUSE_URL);
});
});
describe.each`
enabled | uiData
${true} | ${REPLICATION_STATUS_UI.enabled}
${false} | ${REPLICATION_STATUS_UI.disabled}
`(`conditionally`, ({ enabled, uiData }) => {
beforeEach(() => {
createComponent({ node: { enabled } });
});
describe(`when enabled is ${enabled}`, () => {
it(`renders the replication status icon correctly`, () => {
expect(findReplicationStatusIcon().classes(uiData.color)).toBe(true);
expect(findReplicationStatusIcon().attributes('name')).toBe(uiData.icon);
});
it(`renders the replication status text correctly`, () => {
expect(findReplicationStatusText().classes(uiData.color)).toBe(true);
expect(findReplicationStatusText().text()).toBe(uiData.text);
});
});
});
});
});
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { GlButton, GlCard } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import GeoNodeReplicationStatus from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_status.vue';
import GeoNodeReplicationSummary from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_summary.vue';
import GeoNodeSyncSettings from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_sync_settings.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -11,13 +13,14 @@ describe('GeoNodeReplicationSummary', () => {
node: MOCK_NODES[1],
};
const createComponent = (initialState, props) => {
const createComponent = (props) => {
wrapper = extendedWrapper(
mount(GeoNodeReplicationSummary, {
shallowMount(GeoNodeReplicationSummary, {
propsData: {
...defaultProps,
...props,
},
stubs: { GlCard },
}),
);
};
......@@ -27,9 +30,9 @@ describe('GeoNodeReplicationSummary', () => {
});
const findGlButton = () => wrapper.findComponent(GlButton);
const findGeoNodeReplicationStatus = () => wrapper.findByTestId('replication-status');
const findGeoNodeReplicationStatus = () => wrapper.findComponent(GeoNodeReplicationStatus);
const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts');
const findGeoNodeSyncSettings = () => wrapper.findByTestId('sync-settings');
const findGeoNodeSyncSettings = () => wrapper.findComponent(GeoNodeSyncSettings);
describe('template', () => {
beforeEach(() => {
......
import { shallowMount } from '@vue/test-utils';
import GeoNodeSyncSettings from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_sync_settings.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeSyncSettings', () => {
let wrapper;
const defaultProps = {
node: MOCK_NODES[1],
};
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(GeoNodeSyncSettings, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findSyncType = () => wrapper.findByTestId('sync-type');
const findSyncStatusEventInfo = () => wrapper.findByTestId('sync-status-event-info');
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the sync type', () => {
expect(findSyncType().exists()).toBe(true);
});
});
describe('conditionally', () => {
describe.each`
selectiveSyncType | text
${null} | ${'Full'}
${'namespaces'} | ${'Selective (groups)'}
${'shards'} | ${'Selective (shards)'}
`(`sync type`, ({ selectiveSyncType, text }) => {
beforeEach(() => {
createComponent({ node: { selectiveSyncType } });
});
it(`renders correctly when selectiveSyncType is ${selectiveSyncType}`, () => {
expect(findSyncType().text()).toBe(text);
});
});
describe('with no timestamp info', () => {
beforeEach(() => {
createComponent({ node: { lastEventTimestamp: null, cursorLastEventTimestamp: null } });
});
it('does not render the sync status event info', () => {
expect(findSyncStatusEventInfo().exists()).toBe(false);
});
});
describe('with timestamp info', () => {
beforeEach(() => {
createComponent({
node: {
lastEventTimestamp: 1511255300,
lastEventId: 10,
cursorLastEventTimestamp: 1511255200,
cursorLastEventId: 9,
},
});
});
it('does render the sync status event info', () => {
expect(findSyncStatusEventInfo().exists()).toBe(true);
expect(findSyncStatusEventInfo().text()).toBe('20 seconds (1 events)');
});
});
});
});
});
......@@ -13781,6 +13781,9 @@ msgstr ""
msgid "From the Kubernetes cluster details view, applications list, install GitLab Runner."
msgstr ""
msgid "Full"
msgstr ""
msgid "Full name"
msgstr ""
......@@ -13988,6 +13991,9 @@ msgstr ""
msgid "Geo|%{name} is scheduled for re-verify"
msgstr ""
msgid "Geo|%{timeAgoStr} (%{pendingEvents} events)"
msgstr ""
msgid "Geo|%{title} checksum progress"
msgstr ""
......@@ -14051,6 +14057,9 @@ msgstr ""
msgid "Geo|Geo Status"
msgstr ""
msgid "Geo|Geo nodes are paused using a command run on the node"
msgstr ""
msgid "Geo|Geo sites"
msgstr ""
......@@ -14204,6 +14213,9 @@ msgstr ""
msgid "Geo|Secondary site"
msgstr ""
msgid "Geo|Selective (%{syncLabel})"
msgstr ""
msgid "Geo|Status"
msgstr ""
......@@ -22556,6 +22568,9 @@ msgstr ""
msgid "Pause replication"
msgstr ""
msgid "Paused"
msgstr ""
msgid "Paused runners don't accept new jobs"
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