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> <script>
import { GlCard, GlButton } from '@gitlab/ui'; import { GlCard, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import GeoNodeReplicationStatus from './geo_node_replication_status.vue';
import GeoNodeSyncSettings from './geo_node_sync_settings.vue';
export default { export default {
name: 'GeoNodeReplicationSummary', name: 'GeoNodeReplicationSummary',
...@@ -14,6 +16,8 @@ export default { ...@@ -14,6 +16,8 @@ export default {
components: { components: {
GlCard, GlCard,
GlButton, GlButton,
GeoNodeReplicationStatus,
GeoNodeSyncSettings,
}, },
props: { props: {
node: { node: {
...@@ -38,10 +42,12 @@ export default { ...@@ -38,10 +42,12 @@ export default {
> >
</template> </template>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5"> <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>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5"> <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> </div>
<span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span> <span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span>
</gl-card> </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 { helpPagePath } from '~/helpers/help_page_helper';
import { __ } from '~/locale';
export const GEO_INFO_URL = helpPagePath('administration/geo/index.md'); export const GEO_INFO_URL = helpPagePath('administration/geo/index.md');
...@@ -17,6 +18,10 @@ export const HELP_INFO_URL = helpPagePath( ...@@ -17,6 +18,10 @@ export const HELP_INFO_URL = helpPagePath(
{ anchor: 'repository-verification' }, { anchor: 'repository-verification' },
); );
export const REPLICATION_PAUSE_URL = helpPagePath('administration/geo/index.html', {
anchor: 'pausing-and-resuming-replication',
});
export const HEALTH_STATUS_UI = { export const HEALTH_STATUS_UI = {
healthy: { healthy: {
icon: 'status_success', icon: 'status_success',
...@@ -40,4 +45,17 @@ export const HEALTH_STATUS_UI = { ...@@ -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; 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 { GlButton, GlCard } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; 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 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 { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
...@@ -11,13 +13,14 @@ describe('GeoNodeReplicationSummary', () => { ...@@ -11,13 +13,14 @@ describe('GeoNodeReplicationSummary', () => {
node: MOCK_NODES[1], node: MOCK_NODES[1],
}; };
const createComponent = (initialState, props) => { const createComponent = (props) => {
wrapper = extendedWrapper( wrapper = extendedWrapper(
mount(GeoNodeReplicationSummary, { shallowMount(GeoNodeReplicationSummary, {
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
}, },
stubs: { GlCard },
}), }),
); );
}; };
...@@ -27,9 +30,9 @@ describe('GeoNodeReplicationSummary', () => { ...@@ -27,9 +30,9 @@ describe('GeoNodeReplicationSummary', () => {
}); });
const findGlButton = () => wrapper.findComponent(GlButton); const findGlButton = () => wrapper.findComponent(GlButton);
const findGeoNodeReplicationStatus = () => wrapper.findByTestId('replication-status'); const findGeoNodeReplicationStatus = () => wrapper.findComponent(GeoNodeReplicationStatus);
const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts'); const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts');
const findGeoNodeSyncSettings = () => wrapper.findByTestId('sync-settings'); const findGeoNodeSyncSettings = () => wrapper.findComponent(GeoNodeSyncSettings);
describe('template', () => { describe('template', () => {
beforeEach(() => { 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 "" ...@@ -13781,6 +13781,9 @@ msgstr ""
msgid "From the Kubernetes cluster details view, applications list, install GitLab Runner." msgid "From the Kubernetes cluster details view, applications list, install GitLab Runner."
msgstr "" msgstr ""
msgid "Full"
msgstr ""
msgid "Full name" msgid "Full name"
msgstr "" msgstr ""
...@@ -13988,6 +13991,9 @@ msgstr "" ...@@ -13988,6 +13991,9 @@ msgstr ""
msgid "Geo|%{name} is scheduled for re-verify" msgid "Geo|%{name} is scheduled for re-verify"
msgstr "" msgstr ""
msgid "Geo|%{timeAgoStr} (%{pendingEvents} events)"
msgstr ""
msgid "Geo|%{title} checksum progress" msgid "Geo|%{title} checksum progress"
msgstr "" msgstr ""
...@@ -14051,6 +14057,9 @@ msgstr "" ...@@ -14051,6 +14057,9 @@ msgstr ""
msgid "Geo|Geo Status" msgid "Geo|Geo Status"
msgstr "" msgstr ""
msgid "Geo|Geo nodes are paused using a command run on the node"
msgstr ""
msgid "Geo|Geo sites" msgid "Geo|Geo sites"
msgstr "" msgstr ""
...@@ -14204,6 +14213,9 @@ msgstr "" ...@@ -14204,6 +14213,9 @@ msgstr ""
msgid "Geo|Secondary site" msgid "Geo|Secondary site"
msgstr "" msgstr ""
msgid "Geo|Selective (%{syncLabel})"
msgstr ""
msgid "Geo|Status" msgid "Geo|Status"
msgstr "" msgstr ""
...@@ -22556,6 +22568,9 @@ msgstr "" ...@@ -22556,6 +22568,9 @@ msgstr ""
msgid "Pause replication" msgid "Pause replication"
msgstr "" msgstr ""
msgid "Paused"
msgstr ""
msgid "Paused runners don't accept new jobs" msgid "Paused runners don't accept new jobs"
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