Commit 92571fb2 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '228722-generic-node-status-bars' into 'master'

Geo - Generic Node Status Bars

Closes #228722

See merge request gitlab-org/gitlab!37831
parents 745f2de1 9d48764b
...@@ -43,7 +43,8 @@ export default { ...@@ -43,7 +43,8 @@ export default {
}, },
itemValueType: { itemValueType: {
type: String, type: String,
required: true, required: false,
default: VALUE_TYPE.GRAPH,
}, },
customType: { customType: {
type: String, type: String,
......
<script> <script>
import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui'; import { GlPopover, GlSprintf, GlLink } from '@gitlab/ui';
import { toNumber } from 'lodash';
import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue'; import StackedProgressBar from '~/vue_shared/components/stacked_progress_bar.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
...@@ -22,8 +23,6 @@ export default { ...@@ -22,8 +23,6 @@ export default {
itemValue: { itemValue: {
type: Object, type: Object,
required: true, required: true,
validator: value =>
['totalCount', 'successCount', 'failureCount'].every(key => typeof value[key] === 'number'),
}, },
detailsPath: { detailsPath: {
type: String, type: String,
...@@ -33,7 +32,16 @@ export default { ...@@ -33,7 +32,16 @@ export default {
}, },
computed: { computed: {
queuedCount() { queuedCount() {
return this.itemValue.totalCount - this.itemValue.successCount - this.itemValue.failureCount; return this.totalCount - this.successCount - this.failureCount;
},
totalCount() {
return toNumber(this.itemValue.totalCount) || 0;
},
failureCount() {
return toNumber(this.itemValue.failureCount) || 0;
},
successCount() {
return toNumber(this.itemValue.successCount) || 0;
}, },
}, },
}; };
...@@ -46,9 +54,9 @@ export default { ...@@ -46,9 +54,9 @@ export default {
tabindex="0" tabindex="0"
:hide-tooltips="true" :hide-tooltips="true"
:unavailable-label="__('Nothing to synchronize')" :unavailable-label="__('Nothing to synchronize')"
:success-count="itemValue.successCount" :success-count="successCount"
:failure-count="itemValue.failureCount" :failure-count="failureCount"
:total-count="itemValue.totalCount" :total-count="totalCount"
/> />
<gl-popover <gl-popover
:target="`syncProgress-${itemTitle}`" :target="`syncProgress-${itemTitle}`"
...@@ -67,12 +75,12 @@ export default { ...@@ -67,12 +75,12 @@ export default {
<div class="d-flex align-items-center my-1"> <div class="d-flex align-items-center my-1">
<div class="mr-2 bg-transparent gl-w-5 gl-h-2"></div> <div class="mr-2 bg-transparent gl-w-5 gl-h-2"></div>
<span class="flex-grow-1 mr-3">{{ __('Total') }}</span> <span class="flex-grow-1 mr-3">{{ __('Total') }}</span>
<span class="font-weight-bold">{{ itemValue.totalCount.toLocaleString() }}</span> <span class="font-weight-bold">{{ totalCount.toLocaleString() }}</span>
</div> </div>
<div class="d-flex align-items-center my-2"> <div class="d-flex align-items-center my-2">
<div class="mr-2 bg-success-500 gl-w-5 gl-h-2"></div> <div class="mr-2 bg-success-500 gl-w-5 gl-h-2"></div>
<span class="flex-grow-1 mr-3">{{ __('Synced') }}</span> <span class="flex-grow-1 mr-3">{{ __('Synced') }}</span>
<span class="font-weight-bold">{{ itemValue.successCount.toLocaleString() }}</span> <span class="font-weight-bold">{{ successCount.toLocaleString() }}</span>
</div> </div>
<div class="d-flex align-items-center my-2"> <div class="d-flex align-items-center my-2">
<div class="mr-2 bg-secondary-200 gl-w-5 gl-h-2"></div> <div class="mr-2 bg-secondary-200 gl-w-5 gl-h-2"></div>
...@@ -82,7 +90,7 @@ export default { ...@@ -82,7 +90,7 @@ export default {
<div class="d-flex align-items-center my-2"> <div class="d-flex align-items-center my-2">
<div class="mr-2 bg-danger-500 gl-w-5 gl-h-2"></div> <div class="mr-2 bg-danger-500 gl-w-5 gl-h-2"></div>
<span class="flex-grow-1 mr-3">{{ __('Failed') }}</span> <span class="flex-grow-1 mr-3">{{ __('Failed') }}</span>
<span class="font-weight-bold">{{ itemValue.failureCount.toLocaleString() }}</span> <span class="font-weight-bold">{{ failureCount.toLocaleString() }}</span>
</div> </div>
<div v-if="detailsPath" class="mt-3"> <div v-if="detailsPath" class="mt-3">
<gl-link class="gl-font-sm" :href="detailsPath" target="_blank">{{ <gl-link class="gl-font-sm" :href="detailsPath" target="_blank">{{
......
...@@ -32,56 +32,7 @@ export default { ...@@ -32,56 +32,7 @@ export default {
itemValueType: VALUE_TYPE.CUSTOM, itemValueType: VALUE_TYPE.CUSTOM,
customType: CUSTOM_TYPE.SYNC, customType: CUSTOM_TYPE.SYNC,
}, },
{ ...this.nodeDetails.syncStatuses,
itemEnabled: this.nodeDetails.repositories.enabled,
itemTitle: s__('GeoNodes|Repositories'),
itemValue: this.nodeDetails.repositories,
itemValueType: VALUE_TYPE.GRAPH,
detailsPath: `${this.node.url}admin/geo/projects`,
},
{
itemEnabled: this.nodeDetails.wikis.enabled,
itemTitle: s__('GeoNodes|Wikis'),
itemValue: this.nodeDetails.wikis,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemEnabled: this.nodeDetails.lfs.enabled,
itemTitle: s__('GeoNodes|LFS objects'),
itemValue: this.nodeDetails.lfs,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemEnabled: this.nodeDetails.attachments.enabled,
itemTitle: s__('GeoNodes|Attachments'),
itemValue: this.nodeDetails.attachments,
itemValueType: VALUE_TYPE.GRAPH,
detailsPath: `${this.node.url}admin/geo/uploads`,
},
{
itemEnabled: this.nodeDetails.jobArtifacts.enabled,
itemTitle: s__('GeoNodes|Job artifacts'),
itemValue: this.nodeDetails.jobArtifacts,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemEnabled: this.nodeDetails.containerRepositories.enabled,
itemTitle: s__('GeoNodes|Container repositories'),
itemValue: this.nodeDetails.containerRepositories,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemEnabled: this.nodeDetails.designRepositories.enabled,
itemTitle: s__('GeoNodes|Design repositories'),
itemValue: this.nodeDetails.designRepositories,
itemValueType: VALUE_TYPE.GRAPH,
detailsPath: `${this.node.url}admin/geo/designs`,
},
{
itemTitle: s__('GeoNodes|Package files'),
itemValue: this.nodeDetails.packageFiles,
itemValueType: VALUE_TYPE.GRAPH,
},
{ {
itemTitle: s__('GeoNodes|Data replication lag'), itemTitle: s__('GeoNodes|Data replication lag'),
itemValue: this.dbReplicationLag(), itemValue: this.dbReplicationLag(),
...@@ -140,6 +91,22 @@ export default { ...@@ -140,6 +91,22 @@ export default {
handleSectionToggle(toggleState) { handleSectionToggle(toggleState) {
this.showSectionItems = toggleState; this.showSectionItems = toggleState;
}, },
detailsPath(nodeDetailItem) {
if (!nodeDetailItem.secondaryView) {
return '';
}
// This is due to some legacy coding patterns on the GeoNodeStatus API.
// This will be fixed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/228718
if (nodeDetailItem.itemName === 'repositories') {
return `${this.node.url}admin/geo/replication/projects`;
} else if (nodeDetailItem.itemName === 'attachments') {
return `${this.node.url}admin/geo/replication/uploads`;
}
return `${this.node.url}admin/geo/replication/${nodeDetailItem.itemName}`;
},
}, },
}; };
</script> </script>
...@@ -163,7 +130,7 @@ export default { ...@@ -163,7 +130,7 @@ export default {
:item-value-type="nodeDetailItem.itemValueType" :item-value-type="nodeDetailItem.itemValueType"
:custom-type="nodeDetailItem.customType" :custom-type="nodeDetailItem.customType"
:event-type-log-status="nodeDetailItem.eventTypeLogStatus" :event-type-log-status="nodeDetailItem.eventTypeLogStatus"
:details-path="nodeDetailItem.detailsPath" :details-path="detailsPath(nodeDetailItem)"
/> />
</div> </div>
</div> </div>
......
<script> <script>
import { GlPopover, GlLink, GlIcon, GlSprintf } from '@gitlab/ui'; import { GlPopover, GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale'; import { sprintf, s__ } from '~/locale';
import { VALUE_TYPE, HELP_INFO_URL } from '../../constants'; import { HELP_INFO_URL } from '../../constants';
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';
...@@ -30,64 +30,42 @@ export default { ...@@ -30,64 +30,42 @@ export default {
data() { data() {
return { return {
showSectionItems: false, showSectionItems: false,
primaryNodeDetailItems: this.getPrimaryNodeDetailItems(),
secondaryNodeDetailItems: this.getSecondaryNodeDetailItems(),
}; };
}, },
computed: { computed: {
nodeDetailItems() { nodeDetailItems() {
return this.nodeTypePrimary return this.nodeTypePrimary
? this.getPrimaryNodeDetailItems() ? this.nodeDetails.checksumStatuses
: this.getSecondaryNodeDetailItems(); : this.nodeDetails.verificationStatuses;
}, },
nodeText() { nodeText() {
return this.nodeTypePrimary ? s__('GeoNodes|secondary nodes') : s__('GeoNodes|primary node'); return this.nodeTypePrimary ? s__('GeoNodes|secondary nodes') : s__('GeoNodes|primary node');
}, },
}, },
methods: { methods: {
getPrimaryNodeDetailItems() {
return [
{
itemTitle: s__('GeoNodes|Repository checksum progress'),
itemValue: this.nodeDetails.repositoriesChecksummed,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
},
{
itemTitle: s__('GeoNodes|Wiki checksum progress'),
itemValue: this.nodeDetails.wikisChecksummed,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
},
];
},
getSecondaryNodeDetailItems() {
return [
{
itemTitle: s__('GeoNodes|Repository verification progress'),
itemValue: this.nodeDetails.verifiedRepositories,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
},
{
itemTitle: s__('GeoNodes|Wiki verification progress'),
itemValue: this.nodeDetails.verifiedWikis,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
},
];
},
handleSectionToggle(toggleState) { handleSectionToggle(toggleState) {
this.showSectionItems = toggleState; this.showSectionItems = toggleState;
}, },
itemValue(nodeDetailItem) {
return {
totalCount: nodeDetailItem.itemValue.totalCount,
successCount: this.nodeTypePrimary
? nodeDetailItem.itemValue.checksumSuccessCount
: nodeDetailItem.itemValue.verificationSuccessCount,
failureCount: this.nodeTypePrimary
? nodeDetailItem.itemValue.checksumFailureCount
: nodeDetailItem.itemValue.verificationFailureCount,
};
},
itemTitle(nodeDetailItem) {
return this.nodeTypePrimary
? sprintf(s__('Geo|%{itemTitle} checksum progress'), {
itemTitle: nodeDetailItem.itemTitle,
})
: sprintf(s__('Geo|%{itemTitle} verification progress'), {
itemTitle: nodeDetailItem.itemTitle,
});
},
}, },
HELP_INFO_URL, HELP_INFO_URL,
}; };
...@@ -128,14 +106,8 @@ export default { ...@@ -128,14 +106,8 @@ export default {
<geo-node-detail-item <geo-node-detail-item
v-for="(nodeDetailItem, index) in nodeDetailItems" v-for="(nodeDetailItem, index) in nodeDetailItems"
:key="index" :key="index"
:css-class="nodeDetailItem.cssClass" :item-title="itemTitle(nodeDetailItem)"
:item-title="nodeDetailItem.itemTitle" :item-value="itemValue(nodeDetailItem)"
:item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType"
:success-label="nodeDetailItem.successLabel"
:neutral-label="nodeDetailItem.neutraLabel"
:failure-label="nodeDetailItem.failureLabel"
:custom-type="nodeDetailItem.customType"
/> />
</div> </div>
</template> </template>
......
...@@ -2,7 +2,7 @@ import Vue from 'vue'; ...@@ -2,7 +2,7 @@ import Vue from 'vue';
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GeoNodesStore from './store/geo_nodes_store'; import GeoNodesStore from './store/geo_nodes_store';
import GeoNodesService from './service/geo_nodes_service'; import GeoNodesService from './service/geo_nodes_service';
...@@ -27,9 +27,12 @@ export default () => { ...@@ -27,9 +27,12 @@ export default () => {
data() { data() {
const { dataset } = this.$options.el; const { dataset } = this.$options.el;
const { primaryVersion, primaryRevision, geoTroubleshootingHelpPath } = dataset; const { primaryVersion, primaryRevision, geoTroubleshootingHelpPath } = dataset;
const replicableTypes = convertObjectPropsToCamelCase(JSON.parse(dataset.replicableTypes), {
deep: true,
});
const nodeActionsAllowed = parseBoolean(dataset.nodeActionsAllowed); const nodeActionsAllowed = parseBoolean(dataset.nodeActionsAllowed);
const nodeEditAllowed = parseBoolean(dataset.nodeEditAllowed); const nodeEditAllowed = parseBoolean(dataset.nodeEditAllowed);
const store = new GeoNodesStore(primaryVersion, primaryRevision); const store = new GeoNodesStore(primaryVersion, primaryRevision, replicableTypes);
const service = new GeoNodesService(); const service = new GeoNodesService();
return { return {
......
export default class GeoNodesStore { export default class GeoNodesStore {
constructor(primaryVersion, primaryRevision) { constructor(primaryVersion, primaryRevision, replicableTypes) {
this.state = {}; this.state = {};
this.state.nodes = []; this.state.nodes = [];
this.state.nodeDetails = {}; this.state.nodeDetails = {};
this.state.primaryVersion = primaryVersion; this.state.primaryVersion = primaryVersion;
this.state.primaryRevision = primaryRevision; this.state.primaryRevision = primaryRevision;
this.state.replicableTypes = replicableTypes;
} }
setNodes(nodes) { setNodes(nodes) {
...@@ -16,7 +17,10 @@ export default class GeoNodesStore { ...@@ -16,7 +17,10 @@ export default class GeoNodesStore {
} }
setNodeDetails(nodeId, nodeDetails) { setNodeDetails(nodeId, nodeDetails) {
this.state.nodeDetails[nodeId] = GeoNodesStore.formatNodeDetails(nodeDetails); this.state.nodeDetails[nodeId] = GeoNodesStore.formatNodeDetails(
nodeDetails,
this.state.replicableTypes,
);
} }
removeNode(node) { removeNode(node) {
...@@ -60,7 +64,33 @@ export default class GeoNodesStore { ...@@ -60,7 +64,33 @@ export default class GeoNodesStore {
}; };
} }
static formatNodeDetails(rawNodeDetails) { static formatNodeDetails(rawNodeDetails, replicableTypes) {
const syncStatuses = replicableTypes.map(replicable => {
return {
itemEnabled: rawNodeDetails[`${replicable.namePlural}_replication_enabled`],
itemTitle: replicable.titlePlural,
itemName: replicable.namePlural,
itemValue: {
totalCount: rawNodeDetails[`${replicable.namePlural}_count`],
successCount: rawNodeDetails[`${replicable.namePlural}_synced_count`],
failureCount: rawNodeDetails[`${replicable.namePlural}_failed_count`],
verificationSuccessCount: rawNodeDetails[`${replicable.namePlural}_verified_count`],
verificationFailureCount:
rawNodeDetails[`${replicable.namePlural}_verification_failed_count`],
checksumSuccessCount: rawNodeDetails[`${replicable.namePlural}_checksummed_count`],
checksumFailureCount: rawNodeDetails[`${replicable.namePlural}_checksum_failed_count`],
},
...replicable,
};
});
const verificationStatuses = syncStatuses.filter(s =>
Boolean(s.itemValue.verificationSuccessCount || s.itemValue.verificationFailureCount),
);
const checksumStatuses = syncStatuses.filter(s =>
Boolean(s.itemValue.checksumSuccessCount || s.itemValue.checksumFailureCount),
);
return { return {
id: rawNodeDetails.geo_node_id, id: rawNodeDetails.geo_node_id,
health: rawNodeDetails.health, health: rawNodeDetails.health,
...@@ -81,73 +111,9 @@ export default class GeoNodesStore { ...@@ -81,73 +111,9 @@ export default class GeoNodesStore {
successCount: rawNodeDetails.replication_slots_used_count || 0, successCount: rawNodeDetails.replication_slots_used_count || 0,
failureCount: 0, failureCount: 0,
}, },
repositories: { syncStatuses,
enabled: rawNodeDetails.repositories_replication_enabled, verificationStatuses,
totalCount: rawNodeDetails.projects_count || 0, checksumStatuses,
successCount: rawNodeDetails.repositories_synced_count || 0,
failureCount: rawNodeDetails.repositories_failed_count || 0,
},
wikis: {
enabled: rawNodeDetails.repositories_replication_enabled,
totalCount: rawNodeDetails.projects_count || 0,
successCount: rawNodeDetails.wikis_synced_count || 0,
failureCount: rawNodeDetails.wikis_failed_count || 0,
},
repositoriesChecksummed: {
totalCount: rawNodeDetails.projects_count || 0,
successCount: rawNodeDetails.repositories_checksummed_count || 0,
failureCount: rawNodeDetails.repositories_checksum_failed_count || 0,
},
wikisChecksummed: {
totalCount: rawNodeDetails.projects_count || 0,
successCount: rawNodeDetails.wikis_checksummed_count || 0,
failureCount: rawNodeDetails.wikis_checksum_failed_count || 0,
},
verifiedRepositories: {
totalCount: rawNodeDetails.projects_count || 0,
successCount: rawNodeDetails.repositories_verified_count || 0,
failureCount: rawNodeDetails.repositories_verification_failed_count || 0,
},
verifiedWikis: {
totalCount: rawNodeDetails.projects_count || 0,
successCount: rawNodeDetails.wikis_verified_count || 0,
failureCount: rawNodeDetails.wikis_verification_failed_count || 0,
},
lfs: {
enabled: rawNodeDetails.lfs_objects_replication_enabled,
totalCount: rawNodeDetails.lfs_objects_count || 0,
successCount: rawNodeDetails.lfs_objects_synced_count || 0,
failureCount: rawNodeDetails.lfs_objects_failed_count || 0,
},
jobArtifacts: {
enabled: rawNodeDetails.job_artifacts_replication_enabled,
totalCount: rawNodeDetails.job_artifacts_count || 0,
successCount: rawNodeDetails.job_artifacts_synced_count || 0,
failureCount: rawNodeDetails.job_artifacts_failed_count || 0,
},
containerRepositories: {
enabled: rawNodeDetails.container_repositories_replication_enabled,
totalCount: rawNodeDetails.container_repositories_count || 0,
successCount: rawNodeDetails.container_repositories_synced_count || 0,
failureCount: rawNodeDetails.container_repositories_failed_count || 0,
},
designRepositories: {
enabled: rawNodeDetails.design_repositories_replication_enabled,
totalCount: rawNodeDetails.design_repositories_count || 0,
successCount: rawNodeDetails.design_repositories_synced_count || 0,
failureCount: rawNodeDetails.design_repositories_failed_count || 0,
},
packageFiles: {
totalCount: rawNodeDetails.package_files_registry_count || 0,
successCount: rawNodeDetails.package_files_synced_count || 0,
failureCount: rawNodeDetails.package_files_failed_count || 0,
},
attachments: {
enabled: rawNodeDetails.attachments_replication_enabled,
totalCount: rawNodeDetails.attachments_count || 0,
successCount: rawNodeDetails.attachments_synced_count || 0,
failureCount: rawNodeDetails.attachments_failed_count || 0,
},
lastEvent: { lastEvent: {
id: rawNodeDetails.last_event_id || 0, id: rawNodeDetails.last_event_id || 0,
timeStamp: rawNodeDetails.last_event_timestamp, timeStamp: rawNodeDetails.last_event_timestamp,
......
...@@ -184,8 +184,8 @@ module EE ...@@ -184,8 +184,8 @@ module EE
{ {
title: _('LFS object'), title: _('LFS object'),
title_plural: _('LFS objects'), title_plural: _('LFS objects'),
name: 'lfs', name: 'lfs_object',
name_plural: 'lfs' name_plural: 'lfs_objects'
}, },
{ {
title: _('Attachment'), title: _('Attachment'),
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
mockNodes, mockNodes,
mockNode, mockNode,
rawMockNodeDetails, rawMockNodeDetails,
MOCK_REPLICABLE_TYPES,
} from '../mock_data'; } from '../mock_data';
jest.mock('~/smart_interval'); jest.mock('~/smart_interval');
...@@ -23,7 +24,11 @@ jest.mock('ee/geo_nodes/event_hub'); ...@@ -23,7 +24,11 @@ jest.mock('ee/geo_nodes/event_hub');
const createComponent = () => { const createComponent = () => {
const Component = Vue.extend(appComponent); const Component = Vue.extend(appComponent);
const store = new GeoNodesStore(PRIMARY_VERSION.version, PRIMARY_VERSION.revision); const store = new GeoNodesStore(
PRIMARY_VERSION.version,
PRIMARY_VERSION.revision,
MOCK_REPLICABLE_TYPES,
);
const service = new GeoNodesService(NODE_DETAILS_PATH); const service = new GeoNodesService(NODE_DETAILS_PATH);
return mountComponent(Component, { return mountComponent(Component, {
......
...@@ -57,11 +57,47 @@ describe('GeoNodeSyncProgress', () => { ...@@ -57,11 +57,47 @@ describe('GeoNodeSyncProgress', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe.each`
itemValue | expectedItemValue
${{ successCount: 5, failureCount: 3, totalCount: 10 }} | ${{ successCount: 5, failureCount: 3, totalCount: 10 }}
${{ successCount: '5', failureCount: '3', totalCount: '10' }} | ${{ successCount: 5, failureCount: 3, totalCount: 10 }}
${{ successCount: null, failureCount: null, totalCount: null }} | ${{ successCount: 0, failureCount: 0, totalCount: 0 }}
${{ successCount: 'abc', failureCount: 'def', totalCount: 'ghi' }} | ${{ successCount: 0, failureCount: 0, totalCount: 0 }}
`(`status counts`, ({ itemValue, expectedItemValue }) => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ itemValue });
});
it(`when itemValue.totalCount is ${
itemValue.totalCount
} (${typeof itemValue.totalCount}), it should compute to ${
expectedItemValue.totalCount
}`, () => {
expect(wrapper.vm.totalCount).toBe(expectedItemValue.totalCount);
});
it(`when itemValue.successCount is ${
itemValue.successCount
} (${typeof itemValue.successCount}), it should compute to ${
expectedItemValue.successCount
}`, () => {
expect(wrapper.vm.successCount).toBe(expectedItemValue.successCount);
});
it(`when itemValue.failureCount is ${
itemValue.failureCount
} (${typeof itemValue.failureCount}), it should compute to ${
expectedItemValue.failureCount
}`, () => {
expect(wrapper.vm.failureCount).toBe(expectedItemValue.failureCount);
});
}); });
describe('queuedCount', () => { describe('queuedCount', () => {
beforeEach(() => {
createComponent();
});
it('returns total - success - failure', () => { it('returns total - success - failure', () => {
expect(wrapper.vm.queuedCount).toEqual(MOCK_ITEM_VALUE.queuedCount); expect(wrapper.vm.queuedCount).toEqual(MOCK_ITEM_VALUE.queuedCount);
}); });
......
...@@ -82,6 +82,26 @@ describe('NodeDetailsSectionSync', () => { ...@@ -82,6 +82,26 @@ describe('NodeDetailsSectionSync', () => {
); );
}); });
}); });
describe.each`
nodeDetailItem | path
${{ secondaryView: false, itemName: '' }} | ${''}
${{ secondaryView: true, itemName: 'repositories' }} | ${`${mockNode.url}admin/geo/replication/projects`}
${{ secondaryView: true, itemName: 'attachments' }} | ${`${mockNode.url}admin/geo/replication/uploads`}
${{ secondaryView: true, itemName: 'package_files' }} | ${`${mockNode.url}admin/geo/replication/package_files`}
`(`detailsPath`, ({ nodeDetailItem, path }) => {
describe(`when detail item is ${nodeDetailItem.itemName}`, () => {
let detailPath = '';
beforeEach(() => {
detailPath = wrapper.vm.detailsPath(nodeDetailItem);
});
it(`returns the correct path`, () => {
expect(detailPath).toBe(path);
});
});
});
}); });
describe('template', () => { describe('template', () => {
......
...@@ -3,6 +3,7 @@ import { GlPopover, GlSprintf } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlPopover, GlSprintf } from '@gitlab/ui';
import NodeDetailsSectionVerificationComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_verification.vue'; import NodeDetailsSectionVerificationComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_verification.vue';
import SectionRevealButton from 'ee/geo_nodes/components/node_detail_sections/section_reveal_button.vue'; import SectionRevealButton from 'ee/geo_nodes/components/node_detail_sections/section_reveal_button.vue';
import GeoNodeDetailItem from 'ee/geo_nodes/components/geo_node_detail_item.vue';
import { mockNodeDetails } from '../../mock_data'; import { mockNodeDetails } from '../../mock_data';
...@@ -32,58 +33,11 @@ describe('NodeDetailsSectionVerification', () => { ...@@ -32,58 +33,11 @@ describe('NodeDetailsSectionVerification', () => {
}); });
const findGlPopover = () => wrapper.find(GlPopover); const findGlPopover = () => wrapper.find(GlPopover);
const findDetailItems = () => wrapper.findAll(GeoNodeDetailItem);
describe('data', () => { describe('data', () => {
it('returns default data props', () => { it('returns default data props', () => {
expect(wrapper.vm.showSectionItems).toBe(false); expect(wrapper.vm.showSectionItems).toBe(false);
expect(Array.isArray(wrapper.vm.primaryNodeDetailItems)).toBe(true);
expect(Array.isArray(wrapper.vm.secondaryNodeDetailItems)).toBe(true);
expect(wrapper.vm.primaryNodeDetailItems.length).toBeGreaterThan(0);
expect(wrapper.vm.secondaryNodeDetailItems.length).toBeGreaterThan(0);
});
});
describe('methods', () => {
describe('getPrimaryNodeDetailItems', () => {
const primaryItems = [
{
title: 'Repository checksum progress',
valueProp: 'repositoriesChecksummed',
},
{
title: 'Wiki checksum progress',
valueProp: 'wikisChecksummed',
},
];
it('returns array containing items to show under primary node', () => {
const actualPrimaryItems = wrapper.vm.getPrimaryNodeDetailItems();
primaryItems.forEach((item, index) => {
expect(actualPrimaryItems[index].itemTitle).toBe(item.title);
expect(actualPrimaryItems[index].itemValue).toBe(mockNodeDetails[item.valueProp]);
});
});
});
describe('getSecondaryNodeDetailItems', () => {
const secondaryItems = [
{
title: 'Repository verification progress',
valueProp: 'verifiedRepositories',
},
{
title: 'Wiki verification progress',
valueProp: 'verifiedWikis',
},
];
it('returns array containing items to show under secondary node', () => {
const actualSecondaryItems = wrapper.vm.getSecondaryNodeDetailItems();
secondaryItems.forEach((item, index) => {
expect(actualSecondaryItems[index].itemTitle).toBe(item.title);
expect(actualSecondaryItems[index].itemValue).toBe(mockNodeDetails[item.valueProp]);
});
});
}); });
}); });
...@@ -111,6 +65,50 @@ describe('NodeDetailsSectionVerification', () => { ...@@ -111,6 +65,50 @@ describe('NodeDetailsSectionVerification', () => {
}); });
}); });
describe('methods', () => {
describe.each`
primaryNode | dataKey | nodeDetailItem
${true} | ${'checksum'} | ${{ itemValue: { checksumSuccessCount: 20, checksumFailureCount: 10, verificationSuccessCount: 30, verificationFailureCount: 15 } }}
${false} | ${'verification'} | ${{ itemValue: { totalCount: 100, checksumSuccessCount: 20, checksumFailureCount: 10, verificationSuccessCount: 30, verificationFailureCount: 15 } }}
`(`itemValue`, ({ primaryNode, dataKey, nodeDetailItem }) => {
describe(`when node is ${primaryNode ? 'primary' : 'secondary'}`, () => {
let itemValue = {};
beforeEach(() => {
createComponent({ nodeTypePrimary: primaryNode });
itemValue = wrapper.vm.itemValue(nodeDetailItem);
});
it(`gets successCount correctly`, () => {
expect(itemValue.successCount).toBe(nodeDetailItem.itemValue[`${dataKey}SuccessCount`]);
});
it(`gets failureCount correctly`, () => {
expect(itemValue.failureCount).toBe(nodeDetailItem.itemValue[`${dataKey}FailureCount`]);
});
});
});
describe.each`
primaryNode | itemTitle | titlePostfix
${true} | ${'test'} | ${'checksum progress'}
${false} | ${'test'} | ${'verification progress'}
`(`itemTitle`, ({ primaryNode, itemTitle, titlePostfix }) => {
describe(`when node is ${primaryNode ? 'primary' : 'secondary'}`, () => {
let title = '';
beforeEach(() => {
createComponent({ nodeTypePrimary: primaryNode });
title = wrapper.vm.itemTitle({ itemTitle });
});
it(`creates full title correctly`, () => {
expect(title).toBe(`${itemTitle} ${titlePostfix}`);
});
});
});
});
describe('template', () => { describe('template', () => {
it('renders component container element', () => { it('renders component container element', () => {
expect(wrapper.vm.$el.classList.contains('verification-section')).toBe(true); expect(wrapper.vm.$el.classList.contains('verification-section')).toBe(true);
...@@ -143,5 +141,29 @@ describe('NodeDetailsSectionVerification', () => { ...@@ -143,5 +141,29 @@ describe('NodeDetailsSectionVerification', () => {
).toContain('Replicated data is verified'); ).toContain('Replicated data is verified');
}); });
}); });
describe('GeoNodeDetailItems', () => {
describe('on Primary node', () => {
beforeEach(() => {
createComponent({ nodeTypePrimary: true });
wrapper.vm.showSectionItems = true;
});
it('renders the checksum data', () => {
expect(findDetailItems()).toHaveLength(mockNodeDetails.checksumStatuses.length);
});
});
describe('on Secondary node', () => {
beforeEach(() => {
createComponent({ nodeTypePrimary: false });
wrapper.vm.showSectionItems = true;
});
it('renders the verification data', () => {
expect(findDetailItems()).toHaveLength(mockNodeDetails.verificationStatuses.length);
});
});
});
}); });
}); });
...@@ -182,66 +182,129 @@ export const mockNodeDetails = { ...@@ -182,66 +182,129 @@ export const mockNodeDetails = {
successCount: 1, successCount: 1,
failureCount: 0, failureCount: 0,
}, },
repositories: { syncStatuses: [
{
itemEnabled: true,
itemTitle: 'Repositories',
itemName: 'repositories',
itemValue: {
totalCount: 12, totalCount: 12,
successCount: 12, successCount: 12,
failureCount: 0, failureCount: 0,
verificationSuccessCount: 12,
verificationFailureCount: 0,
checksumSuccessCount: 12,
checksumFailureCount: 0,
},
secondaryView: true,
}, },
wikis: { {
itemEnabled: true,
itemTitle: 'Wikis',
itemName: 'wikis',
itemValue: {
totalCount: 12, totalCount: 12,
successCount: 12, successCount: 12,
failureCount: 0, failureCount: 0,
verificationSuccessCount: 12,
verificationFailureCount: 0,
checksumSuccessCount: 12,
checksumFailureCount: 0,
}, },
lfs: {
totalCount: 0,
successCount: 0,
failureCount: 0,
},
jobArtifacts: {
totalCount: 0,
successCount: 0,
failureCount: 0,
}, },
containerRepositories: { {
totalCount: 0, itemEnabled: true,
itemTitle: 'Designs',
itemName: 'designs',
itemValue: {
totalCount: 25,
successCount: 0, successCount: 0,
failureCount: 0, failureCount: 25,
verificationSuccessCount: null,
verificationFailureCount: null,
checksumSuccessCount: null,
checksumFailureCount: null,
}, },
designRepositories: { secondaryView: true,
totalCount: 0,
successCount: 0,
failureCount: 0,
}, },
packageFiles: { {
totalCount: 25, itemEnabled: true,
successCount: 25, itemTitle: 'Package Files',
failureCount: 0, itemName: 'packageFiles',
itemValue: {
totalCount: 20,
successCount: 12,
failureCount: 8,
verificationSuccessCount: null,
verificationFailureCount: null,
checksumSuccessCount: null,
checksumFailureCount: null,
}, },
attachments: { secondaryView: true,
totalCount: 0,
successCount: 0,
failureCount: 0,
}, },
repositoriesChecksummed: { ],
verificationStatuses: [
{
itemEnabled: true,
itemTitle: 'Repositories',
itemName: 'repositories',
itemValue: {
totalCount: 12, totalCount: 12,
successCount: 12, successCount: 12,
failureCount: 0, failureCount: 0,
verificationSuccessCount: 12,
verificationFailureCount: 0,
checksumSuccessCount: 12,
checksumFailureCount: 0,
},
secondaryView: true,
}, },
wikisChecksummed: { {
itemEnabled: true,
itemTitle: 'Wikis',
itemName: 'wikis',
itemValue: {
totalCount: 12, totalCount: 12,
successCount: 12, successCount: 12,
failureCount: 0, failureCount: 0,
verificationSuccessCount: 12,
verificationFailureCount: 0,
checksumSuccessCount: 12,
checksumFailureCount: 0,
}, },
verifiedRepositories: { },
],
checksumStatuses: [
{
itemEnabled: true,
itemTitle: 'Repositories',
itemName: 'repositories',
itemValue: {
totalCount: 12, totalCount: 12,
successCount: 12, successCount: 12,
failureCount: 0, failureCount: 0,
verificationSuccessCount: 12,
verificationFailureCount: 0,
checksumSuccessCount: 12,
checksumFailureCount: 0,
},
secondaryView: true,
}, },
verifiedWikis: { {
itemEnabled: true,
itemTitle: 'Wikis',
itemName: 'wikis',
itemValue: {
totalCount: 12, totalCount: 12,
successCount: 12, successCount: 12,
failureCount: 0, failureCount: 0,
verificationSuccessCount: 12,
verificationFailureCount: 0,
checksumSuccessCount: 12,
checksumFailureCount: 0,
}, },
},
],
lastEvent: { lastEvent: {
id: 3, id: 3,
timeStamp: 1511255200, timeStamp: 1511255200,
...@@ -254,3 +317,32 @@ export const mockNodeDetails = { ...@@ -254,3 +317,32 @@ export const mockNodeDetails = {
namespaces: [], namespaces: [],
dbReplicationLag: 0, dbReplicationLag: 0,
}; };
export const MOCK_REPLICABLE_TYPES = [
{
title: 'Repository',
titlePlural: 'Repositories',
name: 'repository',
namePlural: 'repositories',
secondaryView: true,
},
{
title: 'Wiki',
titlePlural: 'Wikis',
name: 'wiki',
namePlural: 'wikis',
},
{
title: 'Design',
titlePlural: 'Designs',
name: 'design',
name_plural: 'designs',
secondaryView: true,
},
{
title: 'Package File',
titlePlural: 'Package Files',
name: 'package_file',
namePlural: 'package_files',
},
];
import GeoNodesStore from 'ee/geo_nodes/store/geo_nodes_store'; import GeoNodesStore from 'ee/geo_nodes/store/geo_nodes_store';
import { mockNodes, rawMockNodeDetails, mockNodeDetails } from '../mock_data'; import {
mockNodes,
rawMockNodeDetails,
mockNodeDetails,
MOCK_REPLICABLE_TYPES,
} from '../mock_data';
describe('GeoNodesStore', () => { describe('GeoNodesStore', () => {
let store; let store;
beforeEach(() => { beforeEach(() => {
store = new GeoNodesStore(mockNodeDetails.primaryVersion, mockNodeDetails.primaryRevision); store = new GeoNodesStore(
mockNodeDetails.primaryVersion,
mockNodeDetails.primaryRevision,
MOCK_REPLICABLE_TYPES,
);
}); });
describe('constructor', () => { describe('constructor', () => {
...@@ -16,6 +25,7 @@ describe('GeoNodesStore', () => { ...@@ -16,6 +25,7 @@ describe('GeoNodesStore', () => {
expect(typeof store.state.nodeDetails).toBe('object'); expect(typeof store.state.nodeDetails).toBe('object');
expect(store.state.primaryVersion).toBe(mockNodeDetails.primaryVersion); expect(store.state.primaryVersion).toBe(mockNodeDetails.primaryVersion);
expect(store.state.primaryRevision).toBe(mockNodeDetails.primaryRevision); expect(store.state.primaryRevision).toBe(mockNodeDetails.primaryRevision);
expect(store.state.replicableTypes).toBe(MOCK_REPLICABLE_TYPES);
}); });
}); });
...@@ -60,12 +70,20 @@ describe('GeoNodesStore', () => { ...@@ -60,12 +70,20 @@ describe('GeoNodesStore', () => {
describe('formatNodeDetails', () => { describe('formatNodeDetails', () => {
it('returns formatted raw node details object', () => { it('returns formatted raw node details object', () => {
const nodeDetails = GeoNodesStore.formatNodeDetails(rawMockNodeDetails); const nodeDetails = GeoNodesStore.formatNodeDetails(
rawMockNodeDetails,
store.state.replicableTypes,
);
expect(nodeDetails.healthStatus).toBe(rawMockNodeDetails.health_status); expect(nodeDetails.healthStatus).toBe(rawMockNodeDetails.health_status);
expect(nodeDetails.replicationSlotWAL).toBe( expect(nodeDetails.replicationSlotWAL).toBe(
rawMockNodeDetails.replication_slots_max_retained_wal_bytes, rawMockNodeDetails.replication_slots_max_retained_wal_bytes,
); );
const syncStatusNames = nodeDetails.syncStatuses.map(({ namePlural }) => namePlural);
const replicableTypesNames = store.state.replicableTypes.map(({ namePlural }) => namePlural);
expect(syncStatusNames).toEqual(replicableTypesNames);
}); });
}); });
}); });
...@@ -28,7 +28,7 @@ RSpec.describe EE::GeoHelper do ...@@ -28,7 +28,7 @@ RSpec.describe EE::GeoHelper do
expected_names = %w( expected_names = %w(
repositories repositories
wikis wikis
lfs lfs_objects
attachments attachments
job_artifacts job_artifacts
container_repositories container_repositories
......
...@@ -10963,30 +10963,15 @@ msgstr "" ...@@ -10963,30 +10963,15 @@ msgstr ""
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage." msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr "" msgstr ""
msgid "GeoNodes|Attachments"
msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
msgid "GeoNodes|Consult Geo troubleshooting information" msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr "" msgstr ""
msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag" msgid "GeoNodes|Data replication lag"
msgstr "" msgstr ""
msgid "GeoNodes|Design repositories"
msgstr ""
msgid "GeoNodes|Does not match the primary storage configuration" msgid "GeoNodes|Does not match the primary storage configuration"
msgstr "" msgstr ""
msgid "GeoNodes|Failed"
msgstr ""
msgid "GeoNodes|Full" msgid "GeoNodes|Full"
msgstr "" msgstr ""
...@@ -11002,12 +10987,6 @@ msgstr "" ...@@ -11002,12 +10987,6 @@ msgstr ""
msgid "GeoNodes|Internal URL" msgid "GeoNodes|Internal URL"
msgstr "" msgstr ""
msgid "GeoNodes|Job artifacts"
msgstr ""
msgid "GeoNodes|LFS objects"
msgstr ""
msgid "GeoNodes|Last event ID processed by cursor" msgid "GeoNodes|Last event ID processed by cursor"
msgstr "" msgstr ""
...@@ -11035,12 +11014,6 @@ msgstr "" ...@@ -11035,12 +11014,6 @@ msgstr ""
msgid "GeoNodes|Node's status was updated %{timeAgo}." msgid "GeoNodes|Node's status was updated %{timeAgo}."
msgstr "" msgstr ""
msgid "GeoNodes|Not checksummed"
msgstr ""
msgid "GeoNodes|Package files"
msgstr ""
msgid "GeoNodes|Pausing replication stops the sync process. Are you sure?" msgid "GeoNodes|Pausing replication stops the sync process. Are you sure?"
msgstr "" msgstr ""
...@@ -11062,15 +11035,6 @@ msgstr "" ...@@ -11062,15 +11035,6 @@ msgstr ""
msgid "GeoNodes|Replication status" msgid "GeoNodes|Replication status"
msgstr "" msgstr ""
msgid "GeoNodes|Repositories"
msgstr ""
msgid "GeoNodes|Repository checksum progress"
msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
msgid "GeoNodes|Selective (%{syncLabel})" msgid "GeoNodes|Selective (%{syncLabel})"
msgstr "" msgstr ""
...@@ -11098,27 +11062,12 @@ msgstr "" ...@@ -11098,27 +11062,12 @@ msgstr ""
msgid "GeoNodes|Unused slots" msgid "GeoNodes|Unused slots"
msgstr "" msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
msgid "GeoNodes|Updated %{timeAgo}" msgid "GeoNodes|Updated %{timeAgo}"
msgstr "" msgstr ""
msgid "GeoNodes|Used slots" msgid "GeoNodes|Used slots"
msgstr "" msgstr ""
msgid "GeoNodes|Verified"
msgstr ""
msgid "GeoNodes|Wiki checksum progress"
msgstr ""
msgid "GeoNodes|Wiki verification progress"
msgstr ""
msgid "GeoNodes|Wikis"
msgstr ""
msgid "GeoNodes|With %{geo} you can install a special read-only and replicated instance anywhere. Before you add nodes, follow the %{instructions} in the exact order they appear." msgid "GeoNodes|With %{geo} you can install a special read-only and replicated instance anywhere. Before you add nodes, follow the %{instructions} in the exact order they appear."
msgstr "" msgstr ""
...@@ -11131,6 +11080,12 @@ msgstr "" ...@@ -11131,6 +11080,12 @@ msgstr ""
msgid "GeoNodes|secondary nodes" msgid "GeoNodes|secondary nodes"
msgstr "" msgstr ""
msgid "Geo|%{itemTitle} checksum progress"
msgstr ""
msgid "Geo|%{itemTitle} verification progress"
msgstr ""
msgid "Geo|%{label} can't be blank" msgid "Geo|%{label} can't be blank"
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