Commit 9034b3ba authored by Zack Cuddy's avatar Zack Cuddy Committed by Mike Greiling

Geo Node Status 2.0 - Secondary Details

This change replaces the placeholder
text for the Secondary specific node
deatils with actual components.

This change only focuses on creating
the detail cards and "other" info.

Replication summary and overview
will be done in future MRs.
parent 4c4ba45a
......@@ -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',
},
];
......@@ -13912,9 +13912,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 ""
......@@ -13942,6 +13948,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 ""
......@@ -14026,12 +14038,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 ""
......@@ -14050,9 +14077,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 ""
......@@ -14062,6 +14086,9 @@ msgstr ""
msgid "Geo|Status"
msgstr ""
msgid "Geo|Storage config"
msgstr ""
msgid "Geo|Synced"
msgstr ""
......@@ -14074,6 +14101,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