Commit bf208a35 authored by Mike Greiling's avatar Mike Greiling

Merge branch '287978_06-geo-node-beta-secondary-details' into 'master'

Geo Node Status 2.0 - Secondary Details

See merge request gitlab-org/gitlab!56154
parents fd592398 9034b3ba
......@@ -3,16 +3,20 @@ import { s__ } from '~/locale';
import GeoNodeCoreDetails from './geo_node_core_details.vue';
import GeoNodePrimaryOtherInfo from './primary_node/geo_node_primary_other_info.vue';
import GeoNodeVerificationInfo from './primary_node/geo_node_verification_info.vue';
import GeoNodeReplicationSummary from './secondary_node/geo_node_replication_summary.vue';
import GeoNodeSecondaryOtherInfo from './secondary_node/geo_node_secondary_other_info.vue';
export default {
name: 'GeoNodeDetails',
i18n: {
secondaryDetails: s__('Geo|Secondary Details'),
replicationDetails: s__('Geo|Replication Details'),
},
components: {
GeoNodeCoreDetails,
GeoNodePrimaryOtherInfo,
GeoNodeVerificationInfo,
GeoNodeReplicationSummary,
GeoNodeSecondaryOtherInfo,
},
props: {
node: {
......@@ -37,7 +41,16 @@ export default {
<geo-node-primary-other-info class="gl-flex-fill-1 gl-h-full gl-w-full" :node="node" />
</div>
<div v-else class="gl-display-flex gl-flex-direction-column gl-h-full gl-w-full">
<p data-testid="secondary-node-details">{{ $options.i18n.secondaryDetails }}</p>
<div
class="gl-display-flex gl-sm-flex-direction-column gl-align-items-flex-start gl-h-full gl-w-full gl-mb-5"
>
<geo-node-replication-summary
class="gl-flex-fill-1 gl-mb-5 gl-md-mb-0 gl-md-mr-5 gl-h-full gl-w-full"
:node="node"
/>
<geo-node-secondary-other-info class="gl-flex-fill-1 gl-h-full gl-w-full" :node="node" />
</div>
<p data-testid="secondary-replication-details">{{ $options.i18n.replicationDetails }}</p>
</div>
</div>
</template>
<script>
import { GlCard, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'GeoNodeReplicationSummary',
i18n: {
replicationSummary: s__('Geo|Replication summary'),
replicationDetailsButton: s__('Geo|Replication details'),
replicationStatus: s__('Geo|Replication status'),
syncSettings: s__('Geo|Synchronization settings'),
replicationCounts: s__('Geo|Replication counts'),
},
components: {
GlCard,
GlButton,
},
props: {
node: {
type: Object,
required: true,
},
},
};
</script>
<template>
<gl-card header-class="gl-display-flex gl-align-items-center">
<template #header>
<h5 class="gl-my-0">{{ $options.i18n.replicationSummary }}</h5>
<gl-button
class="gl-ml-auto"
variant="confirm"
category="secondary"
:href="node.webGeoProjectsUrl"
target="_blank"
>{{ $options.i18n.replicationDetailsButton }}</gl-button
>
</template>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span data-testid="replication-status">{{ $options.i18n.replicationStatus }}</span>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span data-testid="sync-settings">{{ $options.i18n.syncSettings }}</span>
</div>
<span data-testid="replication-counts">{{ $options.i18n.replicationCounts }}</span>
</gl-card>
</template>
<script>
import { GlCard } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'GeoNodeSecondaryOtherInfo',
i18n: {
otherInfo: __('Other information'),
dbReplicationLag: s__('Geo|Data replication lag'),
lastEventId: s__('Geo|Last event ID from primary'),
lastCursorEventId: s__('Geo|Last event ID processed by cursor'),
storageConfig: s__('Geo|Storage config'),
shardsNotMatched: s__('Geo|Does not match the primary storage configuration'),
unknown: __('Unknown'),
ok: __('OK'),
},
components: {
GlCard,
TimeAgo,
},
props: {
node: {
type: Object,
required: true,
},
},
computed: {
storageShardsStatus() {
if (this.node.storageShardsMatch == null) {
return this.$options.i18n.unknown;
}
return this.node.storageShardsMatch
? this.$options.i18n.ok
: this.$options.i18n.shardsNotMatched;
},
dbReplicationLag() {
if (parseInt(this.node.dbReplicationLagSeconds, 10) >= 0) {
const parsedTime = parseSeconds(this.node.dbReplicationLagSeconds, {
hoursPerDay: 24,
daysPerWeek: 7,
});
return stringifyTime(parsedTime);
}
return this.$options.i18n.unknown;
},
lastEventTimestamp() {
const time = this.node.lastEventTimestamp * 1000;
return new Date(time).toString();
},
lastCursorEventTimestamp() {
const time = this.node.cursorLastEventTimestamp * 1000;
return new Date(time).toString();
},
},
};
</script>
<template>
<gl-card>
<template #header>
<h5 class="gl-my-3">{{ $options.i18n.otherInfo }}</h5>
</template>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span>{{ $options.i18n.dbReplicationLag }}</span>
<span class="gl-font-weight-bold gl-mt-2" data-testid="replication-lag">{{
dbReplicationLag
}}</span>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span>{{ $options.i18n.lastEventId }}</span>
<span class="gl-font-weight-bold gl-mt-2"
>{{ node.lastEventId || 0 }} (<time-ago
data-testid="last-event"
:time="lastEventTimestamp"
/>)</span
>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span>{{ $options.i18n.lastCursorEventId }}</span>
<span class="gl-font-weight-bold gl-mt-2"
>{{ node.cursorLastEventId || 0 }} (<time-ago
data-testid="last-cursor-event"
:time="lastCursorEventTimestamp"
/>)</span
>
</div>
<div class="gl-display-flex gl-flex-direction-column gl-mb-5">
<span>{{ $options.i18n.storageConfig }}</span>
<span
:class="{ 'gl-text-red-500': !node.storageShardsMatch }"
class="gl-font-weight-bold gl-mt-2"
data-testid="storage-shards"
>{{ storageShardsStatus }}</span
>
</div>
</gl-card>
</template>
......@@ -3,6 +3,8 @@ import GeoNodeCoreDetails from 'ee/geo_nodes_beta/components/details/geo_node_co
import GeoNodeDetails from 'ee/geo_nodes_beta/components/details/geo_node_details.vue';
import GeoNodePrimaryOtherInfo from 'ee/geo_nodes_beta/components/details/primary_node/geo_node_primary_other_info.vue';
import GeoNodeVerificationInfo from 'ee/geo_nodes_beta/components/details/primary_node/geo_node_verification_info.vue';
import GeoNodeReplicationSummary from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_summary.vue';
import GeoNodeSecondaryOtherInfo from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_secondary_other_info.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -31,7 +33,11 @@ describe('GeoNodeDetails', () => {
const findGeoNodeCoreDetails = () => wrapper.findComponent(GeoNodeCoreDetails);
const findGeoNodePrimaryOtherInfo = () => wrapper.findComponent(GeoNodePrimaryOtherInfo);
const findGeoNodeVerificationInfo = () => wrapper.findComponent(GeoNodeVerificationInfo);
const findGeoNodeSecondaryDetails = () => wrapper.findByTestId('secondary-node-details');
const findGeoNodeSecondaryReplicationSummary = () =>
wrapper.findComponent(GeoNodeReplicationSummary);
const findGeoNodeSecondaryOtherInfo = () => wrapper.findComponent(GeoNodeSecondaryOtherInfo);
const findGeoNodeSecondaryReplicationDetails = () =>
wrapper.findByTestId('secondary-replication-details');
describe('template', () => {
describe('always', () => {
......@@ -45,32 +51,39 @@ describe('GeoNodeDetails', () => {
});
describe.each`
node | showPrimaryOtherInfo | showPrimaryVerificationInfo | showSecondaryDetails
${MOCK_NODES[0]} | ${true} | ${true} | ${false}
${MOCK_NODES[1]} | ${false} | ${false} | ${true}
`(
`conditionally`,
({ node, showPrimaryOtherInfo, showPrimaryVerificationInfo, showSecondaryDetails }) => {
beforeEach(() => {
createComponent({ node });
node | showPrimaryComponent | showSecondaryComponent
${MOCK_NODES[0]} | ${true} | ${false}
${MOCK_NODES[1]} | ${false} | ${true}
`(`conditionally`, ({ node, showPrimaryComponent, showSecondaryComponent }) => {
beforeEach(() => {
createComponent({ node });
});
describe(`when primary is ${node.primary}`, () => {
it(`does ${showPrimaryComponent ? '' : 'not '}render GeoNodePrimaryOtherInfo`, () => {
expect(findGeoNodePrimaryOtherInfo().exists()).toBe(showPrimaryComponent);
});
it(`does ${showPrimaryComponent ? '' : 'not '}render GeoNodeVerificationInfo`, () => {
expect(findGeoNodeVerificationInfo().exists()).toBe(showPrimaryComponent);
});
describe(`when primary is ${node.primary}`, () => {
it(`does ${showPrimaryOtherInfo ? '' : 'not '}render GeoNodePrimaryInfo`, () => {
expect(findGeoNodePrimaryOtherInfo().exists()).toBe(showPrimaryOtherInfo);
});
it(`does ${
showSecondaryComponent ? '' : 'not '
}render GeoNodeSecondaryReplicationSummary`, () => {
expect(findGeoNodeSecondaryReplicationSummary().exists()).toBe(showSecondaryComponent);
});
it(`does ${
showPrimaryVerificationInfo ? '' : 'not '
}render GeoNodeVerificationInfo`, () => {
expect(findGeoNodeVerificationInfo().exists()).toBe(showPrimaryVerificationInfo);
});
it(`does ${showSecondaryComponent ? '' : 'not '}render GeoNodeSecondaryOtherInfo`, () => {
expect(findGeoNodeSecondaryOtherInfo().exists()).toBe(showSecondaryComponent);
});
it(`does ${showSecondaryDetails ? '' : 'not '}render GeoNodeSecondaryDetails`, () => {
expect(findGeoNodeSecondaryDetails().exists()).toBe(showSecondaryDetails);
});
it(`does ${
showSecondaryComponent ? '' : 'not '
}render GeoNodeSecondaryReplicationDetails`, () => {
expect(findGeoNodeSecondaryReplicationDetails().exists()).toBe(showSecondaryComponent);
});
},
);
});
});
});
});
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import GeoNodeReplicationSummary from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_replication_summary.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeReplicationSummary', () => {
let wrapper;
const defaultProps = {
node: MOCK_NODES[1],
};
const createComponent = (initialState, props) => {
wrapper = extendedWrapper(
mount(GeoNodeReplicationSummary, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findGlButton = () => wrapper.findComponent(GlButton);
const findGeoNodeReplicationStatus = () => wrapper.findByTestId('replication-status');
const findGeoNodeReplicationCounts = () => wrapper.findByTestId('replication-counts');
const findGeoNodeSyncSettings = () => wrapper.findByTestId('sync-settings');
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders the GlButton as a link', () => {
expect(findGlButton().exists()).toBe(true);
expect(findGlButton().attributes('href')).toBe(MOCK_NODES[1].webGeoProjectsUrl);
});
it('renders the geo node replication status', () => {
expect(findGeoNodeReplicationStatus().exists()).toBe(true);
});
it('renders the geo node replication counts', () => {
expect(findGeoNodeReplicationCounts().exists()).toBe(true);
});
it('renders the geo node sync settings', () => {
expect(findGeoNodeSyncSettings().exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import GeoNodeSecondaryOtherInfo from 'ee/geo_nodes_beta/components/details/secondary_node/geo_node_secondary_other_info.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('GeoNodeSecondaryOtherInfo', () => {
let wrapper;
const defaultProps = {
node: MOCK_NODES[1],
};
const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(GeoNodeSecondaryOtherInfo, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findDbReplicationLag = () => wrapper.findByTestId('replication-lag');
const findLastEvent = () => wrapper.findByTestId('last-event');
const findLastCursorEvent = () => wrapper.findByTestId('last-cursor-event');
const findStorageShards = () => wrapper.findByTestId('storage-shards');
describe('template', () => {
describe('always', () => {
beforeEach(() => {
createComponent();
});
it('renders the db replication lag', () => {
expect(findDbReplicationLag().exists()).toBe(true);
});
it('renders the last event correctly', () => {
expect(findLastEvent().exists()).toBe(true);
expect(findLastEvent().props('time')).toBe(
new Date(MOCK_NODES[1].lastEventTimestamp * 1000).toString(),
);
});
it('renders the last cursor event correctly', () => {
expect(findLastCursorEvent().exists()).toBe(true);
expect(findLastCursorEvent().props('time')).toBe(
new Date(MOCK_NODES[1].cursorLastEventTimestamp * 1000).toString(),
);
});
it('renders the storage shards', () => {
expect(findStorageShards().exists()).toBe(true);
});
});
describe('conditionally', () => {
describe.each`
dbReplicationLagSeconds | text
${60} | ${'1m'}
${null} | ${'Unknown'}
`(`db replication lag`, ({ dbReplicationLagSeconds, text }) => {
beforeEach(() => {
createComponent({ node: { dbReplicationLagSeconds } });
});
it(`renders correctly when dbReplicationLagSeconds is ${dbReplicationLagSeconds}`, () => {
expect(findDbReplicationLag().text()).toBe(text);
});
});
describe.each`
storageShardsMatch | text | hasErrorClass
${true} | ${'OK'} | ${false}
${false} | ${'Does not match the primary storage configuration'} | ${true}
`(`storage shards`, ({ storageShardsMatch, text, hasErrorClass }) => {
beforeEach(() => {
createComponent({ node: { storageShardsMatch } });
});
it(`renders correctly when storageShardsMatch is ${storageShardsMatch}`, () => {
expect(findStorageShards().text()).toBe(text);
expect(findStorageShards().classes('gl-text-red-500')).toBe(hasErrorClass);
});
});
});
});
});
......@@ -173,6 +173,7 @@ export const MOCK_NODES = [
version: '10.4.0-pre',
revision: 'b93c51849b',
storageShardsMatch: true,
webGeoProjectsUrl: 'http://127.0.0.1:3002/replication/projects',
},
];
......@@ -235,5 +236,6 @@ export const MOCK_NODE_STATUSES_RES = [
version: '10.4.0-pre',
revision: 'b93c51849b',
storage_shards_match: true,
web_geo_projects_url: 'http://127.0.0.1:3002/replication/projects',
},
];
......@@ -13910,9 +13910,15 @@ msgstr ""
msgid "Geo|Could not remove tracking entry for an existing upload."
msgstr ""
msgid "Geo|Data replication lag"
msgstr ""
msgid "Geo|Discover GitLab Geo"
msgstr ""
msgid "Geo|Does not match the primary storage configuration"
msgstr ""
msgid "Geo|Failed"
msgstr ""
......@@ -13940,6 +13946,12 @@ msgstr ""
msgid "Geo|Internal URL"
msgstr ""
msgid "Geo|Last event ID from primary"
msgstr ""
msgid "Geo|Last event ID processed by cursor"
msgstr ""
msgid "Geo|Last repository check run"
msgstr ""
......@@ -14024,12 +14036,27 @@ msgstr ""
msgid "Geo|Replicated data is verified with the secondary node(s) using checksums."
msgstr ""
msgid "Geo|Replication Details"
msgstr ""
msgid "Geo|Replication counts"
msgstr ""
msgid "Geo|Replication details"
msgstr ""
msgid "Geo|Replication slot WAL"
msgstr ""
msgid "Geo|Replication slots"
msgstr ""
msgid "Geo|Replication status"
msgstr ""
msgid "Geo|Replication summary"
msgstr ""
msgid "Geo|Resync"
msgstr ""
......@@ -14048,9 +14075,6 @@ msgstr ""
msgid "Geo|Review replication status, and resynchronize and reverify items with the primary node."
msgstr ""
msgid "Geo|Secondary Details"
msgstr ""
msgid "Geo|Secondary node"
msgstr ""
......@@ -14060,6 +14084,9 @@ msgstr ""
msgid "Geo|Status"
msgstr ""
msgid "Geo|Storage config"
msgstr ""
msgid "Geo|Synced"
msgstr ""
......@@ -14072,6 +14099,9 @@ msgstr ""
msgid "Geo|Synchronization of %{itemTitle} is disabled."
msgstr ""
msgid "Geo|Synchronization settings"
msgstr ""
msgid "Geo|The database is currently %{db_lag} behind the primary node."
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