Commit 0ee47266 authored by Phil Hughes's avatar Phil Hughes

Merge branch '5146-geo-nodes-redesign' into 'master'

Update Geo nodes layout for better usability

Closes #5146

See merge request gitlab-org/gitlab-ee!5199
parents 6528ee7d 3905b406
......@@ -10,13 +10,13 @@ import eventHub from '../event_hub';
import { NODE_ACTIONS } from '../constants';
import geoNodesList from './geo_nodes_list.vue';
import GeoNodeItem from './geo_node_item.vue';
export default {
components: {
loadingIcon,
DeprecatedModal,
geoNodesList,
GeoNodeItem,
},
props: {
store: {
......@@ -46,7 +46,6 @@ export default {
modalKind: 'warning',
modalMessage: '',
modalActionLabel: '',
errorMessage: '',
};
},
computed: {
......@@ -85,7 +84,6 @@ export default {
});
},
fetchGeoNodes() {
this.hasError = false;
this.service
.getGeoNodes()
.then(res => res.data)
......@@ -93,9 +91,11 @@ export default {
this.store.setNodes(nodes);
this.isLoading = false;
})
.catch(err => {
this.hasError = true;
this.errorMessage = err;
.catch(() => {
this.isLoading = false;
Flash(
s__('GeoNodes|Something went wrong while fetching nodes'),
);
});
},
fetchNodeDetails(node) {
......@@ -217,28 +217,21 @@ export default {
</script>
<template>
<div class="panel panel-default">
<div class="panel-heading">
Geo nodes ({{ nodes.length }})
</div>
<div class="geo-nodes-container">
<loading-icon
class="loading-animation prepend-top-20 append-bottom-20"
size="2"
v-if="isLoading"
:label="s__('GeoNodes|Loading nodes')"
/>
<geo-nodes-list
v-if="!isLoading"
:nodes="nodes"
<geo-node-item
v-for="(node, index) in nodes"
:key="index"
:node="node"
:primary-node="node.primary"
:node-actions-allowed="nodeActionsAllowed"
:node-edit-allowed="nodeEditAllowed"
/>
<p
class="health-message prepend-left-15 append-right-15"
v-if="hasError"
>
{{ errorMessage }}
</p>
<deprecated-modal
v-show="showModal"
:title="__('Are you sure?')"
......
......@@ -4,14 +4,12 @@
import { VALUE_TYPE, CUSTOM_TYPE } from '../constants';
import geoNodeHealthStatus from './geo_node_health_status.vue';
import geoNodeSyncSettings from './geo_node_sync_settings.vue';
import geoNodeEventStatus from './geo_node_event_status.vue';
export default {
components: {
stackedProgressBar,
geoNodeHealthStatus,
geoNodeSyncSettings,
geoNodeEventStatus,
},
......@@ -53,6 +51,11 @@
required: false,
default: '',
},
eventTypeLogStatus: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
isValueTypePlain() {
......@@ -64,9 +67,6 @@
isValueTypeCustom() {
return this.itemValueType === VALUE_TYPE.CUSTOM;
},
isCustomTypeStatus() {
return this.customType === CUSTOM_TYPE.STATUS;
},
isCustomTypeSync() {
return this.customType === CUSTOM_TYPE.SYNC;
},
......@@ -75,7 +75,7 @@
</script>
<template>
<li class="row node-detail-item">
<div class="node-detail-item prepend-top-15 prepend-left-10">
<div class="node-detail-title">
{{ itemTitle }}
</div>
......@@ -100,12 +100,8 @@
/>
</div>
<template v-if="isValueTypeCustom">
<geo-node-health-status
v-if="isCustomTypeStatus"
:status="itemValue"
/>
<geo-node-sync-settings
v-else-if="isCustomTypeSync"
v-if="isCustomTypeSync"
:sync-status-unavailable="itemValue.syncStatusUnavailable"
:selective-sync-type="itemValue.selectiveSyncType"
:last-event="itemValue.lastEvent"
......@@ -115,7 +111,8 @@
v-else
:event-id="itemValue.eventId"
:event-time-stamp="itemValue.eventTimeStamp"
:event-type-log-status="eventTypeLogStatus"
/>
</template>
</li>
</div>
</template>
<script>
import { s__, sprintf } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
import tooltip from '~/vue_shared/directives/tooltip';
......@@ -18,6 +19,12 @@
eventTimeStamp: {
type: Number,
required: true,
default: 0,
},
eventTypeLogStatus: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
......@@ -27,6 +34,14 @@
timeStampString() {
return formatDate(this.timeStamp);
},
eventString() {
if (this.eventTypeLogStatus) {
return sprintf(s__('GeoNodes|%{eventId} events behind'), {
eventId: this.eventId,
});
}
return this.eventId;
},
},
};
</script>
......@@ -37,7 +52,7 @@
>
<template v-if="eventTimeStamp">
<strong>
{{ eventId }}
{{ eventString }}
</strong>
<span
v-tooltip
......
<script>
import { s__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
icon,
loadingIcon,
},
directives: {
tooltip,
},
props: {
node: {
type: Object,
required: true,
},
nodeDetails: {
type: Object,
required: true,
},
nodeDetailsLoading: {
type: Boolean,
required: true,
},
nodeDetailsFailed: {
type: Boolean,
required: true,
},
},
computed: {
isNodeHTTP() {
return this.node.url.startsWith('http://');
},
showNodeStatusIcon() {
if (this.nodeDetailsLoading) {
return false;
}
return this.isNodeHTTP || this.nodeDetailsFailed;
},
nodeStatusIconClass() {
const iconClasses = 'prepend-left-10 pull-left node-status-icon';
if (this.nodeDetailsFailed) {
return `${iconClasses} status-icon-failure`;
}
return `${iconClasses} status-icon-warning`;
},
nodeStatusIconName() {
if (this.nodeDetailsFailed) {
return 'status_failed_borderless';
}
return 'warning';
},
nodeStatusIconTooltip() {
if (this.nodeDetailsFailed) {
return '';
}
return s__('GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS.');
},
},
};
</script>
<template>
<div class="panel-heading">
<div class="row">
<div class="col-md-8 clearfix">
<strong class="node-url inline pull-left">
{{ node.url }}
</strong>
<loading-icon
v-if="nodeDetailsLoading || node.nodeActionActive"
class="node-details-loading prepend-left-10 pull-left inline"
/>
<icon
v-tooltip
v-if="showNodeStatusIcon"
data-container="body"
data-placement="bottom"
:name="nodeStatusIconName"
:size="18"
:css-classes="nodeStatusIconClass"
:title="nodeStatusIconTooltip"
/>
<span class="inline pull-left prepend-left-10">
<span
class="prepend-left-5 node-badge current-node"
v-if="node.current"
>
{{ s__('Current node') }}
</span>
<span
class="prepend-left-5 node-badge primary-node"
v-if="node.primary"
>
{{ s__('Primary') }}
</span>
</span>
</div>
</div>
</div>
</template>
......@@ -24,18 +24,23 @@
</script>
<template>
<div
class="node-detail-value node-health-status"
:class="healthCssClass"
>
<icon
:size="16"
:name="statusIconName"
/>
<span
class="status-text prepend-left-5"
<div class="prepend-top-15 detail-section-item">
<div class="node-detail-title">
{{ s__('GeoNodes|Health status:') }}
</div>
<div
class="node-detail-value node-health-status"
:class="healthCssClass"
>
{{ status }}
</span>
<icon
:size="16"
:name="statusIconName"
/>
<span
class="status-text prepend-left-5"
>
{{ status }}
</span>
</div>
</div>
</template>
<script>
import { s__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../event_hub';
import geoNodeActions from './geo_node_actions.vue';
import geoNodeDetails from './geo_node_details.vue';
import GeoNodeHeader from './geo_node_header.vue';
import GeoNodeDetails from './geo_node_details.vue';
export default {
components: {
icon,
loadingIcon,
geoNodeActions,
geoNodeDetails,
},
directives: {
tooltip,
GeoNodeHeader,
GeoNodeDetails,
},
props: {
node: {
......@@ -47,41 +37,12 @@ export default {
};
},
computed: {
isNodeNonHTTPS() {
return this.node.url.startsWith('http://');
},
showNodeStatusIcon() {
if (this.isNodeDetailsLoading) {
return false;
}
return this.isNodeNonHTTPS || this.isNodeDetailsFailed;
},
showNodeDetails() {
if (!this.isNodeDetailsLoading) {
return !this.isNodeDetailsFailed;
}
return false;
},
nodeStatusIconClass() {
const iconClasses = 'prepend-left-10 pull-left';
if (this.isNodeDetailsFailed) {
return `${iconClasses} node-status-icon-failure`;
}
return `${iconClasses} node-status-icon-warning`;
},
nodeStatusIconName() {
if (this.isNodeDetailsFailed) {
return 'status_failed_borderless';
}
return 'warning';
},
nodeStatusIconTooltip() {
if (this.isNodeDetailsFailed) {
return '';
}
return s__('GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS.');
},
},
created() {
eventHub.$on('nodeDetailsLoaded', this.handleNodeDetails);
......@@ -119,59 +80,22 @@ export default {
</script>
<template>
<li
<div
class="panel panel-default geo-node-item"
:class="{ 'node-action-active': node.nodeActionActive }"
>
<div class="row">
<div class="col-md-8">
<div class="row">
<div class="col-md-8 clearfix">
<strong class="node-url inline pull-left">
{{ node.url }}
</strong>
<loading-icon
v-if="isNodeDetailsLoading || node.nodeActionActive"
class="node-details-loading prepend-left-10 pull-left inline"
size="1"
/>
<icon
v-tooltip
v-if="showNodeStatusIcon"
data-container="body"
data-placement="bottom"
:name="nodeStatusIconName"
:size="18"
:css-classes="nodeStatusIconClass"
:title="nodeStatusIconTooltip"
/>
<span class="inline pull-left prepend-left-10">
<span
class="node-badge current-node"
v-if="node.current"
>
{{ s__('Current node') }}
</span>
<span
class="node-badge primary-node"
v-if="node.primary"
>
{{ s__('Primary') }}
</span>
</span>
</div>
</div>
</div>
<geo-node-actions
v-if="showNodeDetails && nodeActionsAllowed"
:node="node"
:node-edit-allowed="nodeEditAllowed"
:node-missing-oauth="nodeDetails.missingOAuthApplication"
/>
</div>
<geo-node-header
:node="node"
:node-details="nodeDetails"
:node-details-loading="isNodeDetailsLoading"
:node-details-failed="isNodeDetailsFailed"
/>
<geo-node-details
v-if="showNodeDetails"
:node="node"
:node-details="nodeDetails"
:node-edit-allowed="nodeEditAllowed"
:node-actions-allowed="nodeActionsAllowed"
/>
<div
v-if="isNodeDetailsFailed"
......@@ -181,5 +105,5 @@ export default {
{{ errorMessage }}
</p>
</div>
</li>
</div>
</template>
......@@ -118,7 +118,7 @@
<span
v-else
v-tooltip
class="node-sync-settings inline"
class="node-sync-settings"
data-placement="bottom"
:title="syncStatusTooltip"
>
......
......@@ -23,7 +23,7 @@ export default {
</script>
<template>
<ul class="well-list geo-nodes">
<div class="panel panel-default">
<geo-node-item
v-for="(node, index) in nodes"
:key="index"
......@@ -32,5 +32,5 @@ export default {
:node-actions-allowed="nodeActionsAllowed"
:node-edit-allowed="nodeEditAllowed"
/>
</ul>
</div>
</template>
<script>
import { __ } from '~/locale';
import GeoNodeHealthStatus from '../geo_node_health_status.vue';
import GeoNodeActions from '../geo_node_actions.vue';
export default {
components: {
GeoNodeHealthStatus,
GeoNodeActions,
},
props: {
node: {
type: Object,
required: true,
},
nodeDetails: {
type: Object,
required: true,
},
nodeActionsAllowed: {
type: Boolean,
required: true,
},
nodeEditAllowed: {
type: Boolean,
required: true,
},
versionMismatch: {
type: Boolean,
required: true,
},
},
computed: {
nodeVersion() {
if (this.nodeDetails.version == null &&
this.nodeDetails.revision == null) {
return __('Unknown');
}
return `${this.nodeDetails.version} (${this.nodeDetails.revision})`;
},
nodeHealthStatus() {
return this.nodeDetails.healthy ? this.nodeDetails.health : this.nodeDetails.healthStatus;
},
},
};
</script>
<template>
<div class="row-fluid clearfix node-detail-section primary-section">
<div class="col-md-8">
<div class="detail-section-item node-version">
<div class="node-detail-title">
{{ s__('GeoNodes|GitLab version:') }}
</div>
<div
class="node-detail-value node-detail-value-bold"
:class="{ 'node-detail-value-error': versionMismatch }"
>
{{ nodeVersion }}
</div>
</div>
<geo-node-health-status
:status="nodeHealthStatus"
/>
</div>
<geo-node-actions
v-if="nodeActionsAllowed"
:node="node"
:node-edit-allowed="nodeEditAllowed"
:node-missing-oauth="nodeDetails.missingOAuthApplication"
/>
</div>
</template>
<script>
import { s__, __ } from '~/locale';
import { VALUE_TYPE } from '../../constants';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
export default {
valueType: VALUE_TYPE,
components: {
SectionRevealButton,
GeoNodeDetailItem,
},
props: {
nodeDetails: {
type: Object,
required: true,
},
},
data() {
return {
showSectionItems: false,
};
},
computed: {
storageShardsStatus() {
if (this.nodeDetails.storageShardsMatch == null) {
return __('Unknown');
}
return this.nodeDetails.storageShardsMatch ? __('OK') : s__('GeoNodes|Does not match the primary storage configuration');
},
storageShardsCssClass() {
const cssClass = 'node-detail-value-bold';
return !this.nodeDetails.storageShardsMatch ? `${cssClass} node-detail-value-error` : cssClass;
},
},
methods: {
handleSectionToggle(toggleState) {
this.showSectionItems = toggleState;
},
},
};
</script>
<template>
<div class="row-fluid clearfix node-detail-section other-section">
<div class="col-md-12">
<section-reveal-button
:button-title="__('Other information')"
@toggleButton="handleSectionToggle"
/>
</div>
<div
v-show="showSectionItems"
class="col-md-6 prepend-left-15 prepend-top-10 section-items-container"
>
<geo-node-detail-item
:item-title="s__('GeoNodes|Storage config:')"
:item-value="storageShardsStatus"
:item-value-type="$options.valueType.PLAIN"
:css-class="storageShardsCssClass"
/>
</div>
</div>
</template>
<script>
import { s__, __ } from '~/locale';
import { parseSeconds, stringifyTime } from '~/lib/utils/pretty_time';
import { VALUE_TYPE, CUSTOM_TYPE } from '../../constants';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
export default {
components: {
SectionRevealButton,
GeoNodeDetailItem,
},
props: {
nodeDetails: {
type: Object,
required: true,
},
},
data() {
return {
showSectionItems: false,
nodeDetailItems: [
{
itemTitle: s__('GeoNodes|Sync settings:'),
itemValue: this.syncSettings(),
itemValueType: VALUE_TYPE.CUSTOM,
customType: CUSTOM_TYPE.SYNC,
},
{
itemTitle: s__('GeoNodes|Repositories:'),
itemValue: this.nodeDetails.repositories,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemTitle: s__('GeoNodes|Wikis:'),
itemValue: this.nodeDetails.wikis,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemTitle: s__('GeoNodes|Local LFS objects:'),
itemValue: this.nodeDetails.lfs,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemTitle: s__('GeoNodes|Local attachments:'),
itemValue: this.nodeDetails.attachments,
itemValueType: VALUE_TYPE.GRAPH,
},
{
itemTitle: s__('GeoNodes|Data replication lag:'),
itemValue: this.dbReplicationLag(),
itemValueType: VALUE_TYPE.PLAIN,
},
{
itemTitle: s__('GeoNodes|Last event ID seen from primary:'),
itemValue: this.lastEventStatus(),
itemValueType: VALUE_TYPE.CUSTOM,
customType: CUSTOM_TYPE.EVENT,
},
{
itemTitle: s__('GeoNodes|Latest event log status:'),
itemValue: this.cursorLastEventStatus(),
itemValueType: VALUE_TYPE.CUSTOM,
customType: CUSTOM_TYPE.EVENT,
eventTypeLogStatus: true,
},
],
};
},
methods: {
syncSettings() {
return {
syncStatusUnavailable: this.nodeDetails.syncStatusUnavailable,
selectiveSyncType: this.nodeDetails.selectiveSyncType,
lastEvent: this.nodeDetails.lastEvent,
cursorLastEvent: this.nodeDetails.cursorLastEvent,
};
},
dbReplicationLag() {
// Replication lag can be nil if the secondary isn't actually streaming
if (this.nodeDetails.dbReplicationLag !== null &&
this.nodeDetails.dbReplicationLag >= 0) {
const parsedTime = parseSeconds(this.nodeDetails.dbReplicationLag, {
hoursPerDay: 24,
daysPerWeek: 7,
});
return stringifyTime(parsedTime);
}
return __('Unknown');
},
lastEventStatus() {
return {
eventId: this.nodeDetails.lastEvent.id,
eventTimeStamp: this.nodeDetails.lastEvent.timeStamp,
};
},
cursorLastEventStatus() {
return {
eventId: this.nodeDetails.cursorLastEvent.id,
eventTimeStamp: this.nodeDetails.cursorLastEvent.timeStamp,
};
},
handleSectionToggle(toggleState) {
this.showSectionItems = toggleState;
},
},
};
</script>
<template>
<div class="row-fluid clearfix node-detail-section sync-section">
<div class="col-md-12">
<section-reveal-button
:button-title="__('Sync information')"
@toggleButton="handleSectionToggle"
/>
</div>
<div
v-show="showSectionItems"
class="col-md-6 prepend-left-15 prepend-top-10 section-items-container"
>
<geo-node-detail-item
v-for="(nodeDetailItem, index) in nodeDetailItems"
:key="index"
:css-class="nodeDetailItem.cssClass"
:item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType"
:custom-type="nodeDetailItem.customType"
:event-type-log-status="nodeDetailItem.eventTypeLogStatus"
/>
</div>
</div>
</template>
<script>
import { s__ } from '~/locale';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { VALUE_TYPE } from '../../constants';
import GeoNodeDetailItem from '../geo_node_detail_item.vue';
import SectionRevealButton from './section_reveal_button.vue';
export default {
components: {
GeoNodeDetailItem,
SectionRevealButton,
},
props: {
nodeDetails: {
type: Object,
required: true,
},
nodeTypePrimary: {
type: Boolean,
required: true,
},
},
data() {
return {
showSectionItems: false,
primaryNodeDetailItems: this.getPrimaryNodeDetailItems(),
secondaryNodeDetailItems: this.getSecondaryNodeDetailItems(),
};
},
computed: {
hasItemsToShow() {
if (!this.nodeTypePrimary) {
return this.nodeDetails.repositoryVerificationEnabled;
}
return true;
},
nodeDetailItems() {
return this.nodeTypePrimary ?
this.getPrimaryNodeDetailItems() :
this.getSecondaryNodeDetailItems();
},
},
methods: {
getPrimaryNodeDetailItems() {
const primaryNodeDetailItems = [];
if (this.nodeDetails.repositoryVerificationEnabled) {
primaryNodeDetailItems.push(
{
itemTitle: s__('GeoNodes|Repository verification progress:'),
itemValue: this.nodeDetails.verifiedRepositories,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
},
{
itemTitle: s__('GeoNodes|Wikis checksums calculated verifies:'),
itemValue: this.nodeDetails.verifiedWikis,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Checksummed'),
neutraLabel: s__('GeoNodes|Not checksummed'),
failureLabel: s__('GeoNodes|Failed'),
},
);
}
primaryNodeDetailItems.push(
{
itemTitle: s__('GeoNodes|Replication slots:'),
itemValue: this.nodeDetails.replicationSlots,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Used slots'),
neutraLabel: s__('GeoNodes|Unused slots'),
},
);
if (this.nodeDetails.replicationSlots.totalCount) {
primaryNodeDetailItems.push(
{
itemTitle: s__('GeoNodes|Replication slot WAL:'),
itemValue: numberToHumanSize(this.nodeDetails.replicationSlotWAL),
itemValueType: VALUE_TYPE.PLAIN,
cssClass: 'node-detail-value-bold',
},
);
}
return primaryNodeDetailItems;
},
getSecondaryNodeDetailItems() {
const secondaryNodeDetailItems = [
{
itemTitle: s__('GeoNodes|Repository checksums verified:'),
itemValue: this.nodeDetails.verifiedRepositories,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
},
{
itemTitle: s__('GeoNodes|Wiki checksums verified:'),
itemValue: this.nodeDetails.verifiedWikis,
itemValueType: VALUE_TYPE.GRAPH,
successLabel: s__('GeoNodes|Verified'),
neutraLabel: s__('GeoNodes|Unverified'),
failureLabel: s__('GeoNodes|Failed'),
},
];
return secondaryNodeDetailItems;
},
handleSectionToggle(toggleState) {
this.showSectionItems = toggleState;
},
},
};
</script>
<template>
<div
v-if="hasItemsToShow"
class="row-fluid clearfix node-detail-section verification-section"
>
<div class="col-md-12">
<section-reveal-button
:button-title="__('Verification information')"
@toggleButton="handleSectionToggle"
/>
</div>
<template v-if="showSectionItems">
<div
class="col-md-6 prepend-left-15 prepend-top-10 section-items-container"
>
<geo-node-detail-item
v-for="(nodeDetailItem, index) in nodeDetailItems"
:key="index"
:css-class="nodeDetailItem.cssClass"
:item-title="nodeDetailItem.itemTitle"
: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>
</template>
</div>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
buttonTitle: {
type: String,
required: true,
},
},
data() {
return {
toggleState: false,
};
},
computed: {
toggleButtonIcon() {
return this.toggleState ? 'angle-up' : 'angle-down';
},
},
methods: {
onClickButton() {
this.toggleState = !this.toggleState;
this.$emit('toggleButton', this.toggleState);
},
},
};
</script>
<template>
<button
class="btn-link btn-show-section"
type="button"
@click="onClickButton"
>
<icon
:size="16"
:name="toggleButtonIcon"
/>
<span class="prepend-left-8">{{ buttonTitle }}</span>
</button>
</template>
......@@ -11,40 +11,16 @@
.page-subtitle {
margin-bottom: 24px;
}
.health-message {
padding: 2px 8px;
background-color: $red-100;
color: $red-500;
border-radius: $border-radius-default;
font-weight: normal;
}
}
.well-list.geo-nodes {
li {
position: relative;
&:hover {
background: $white-light;
}
&.node-action-active {
pointer-events: none;
opacity: 0.5;
}
}
}
.node-badge {
color: $white-light;
display: inline-block;
margin-left: 5px;
padding: 0 5px;
border-radius: 3px;
padding: 1px $gl-padding-8;
font-size: $label-font-size;
border-radius: $label-border-radius;
&.primary-node {
background-color: $blue-300;
background-color: $blue-600;
}
&.current-node {
......@@ -72,30 +48,57 @@
color: $gray-darkest;
}
.geo-nodes {
.node-status-icon-warning {
.geo-node-item {
.node-status-icon {
height: 35px;
}
.status-icon-warning {
fill: $gl-warning;
}
.node-status-icon-failure {
.status-icon-failure {
fill: $gl-danger;
}
}
.node-details-list {
.node-detail-item {
margin-top: 14px;
.geo-node-item {
.panel-body {
padding: 0;
&:first-child {
margin-top: 0;
.node-detail-section {
padding: $gl-padding 0;
}
.node-detail-title,
.node-detail-value {
padding-left: $gl-col-padding;
padding-right: $gl-col-padding;
.node-detail-section {
&.sync-section,
&.verification-section,
&.other-section {
border-top: 1px solid $border-color;
}
.btn-show-section {
padding: 0;
}
}
.node-health-message-container {
max-height: $dropdown-max-height;
overflow-y: auto;
.node-health-message {
margin-bottom: 0;
padding: 2px $gl-padding-8;
background-color: $red-100;
color: $red-500;
}
}
}
}
.node-detail-section {
.detail-section-item,
.section-items-container {
.node-detail-title {
color: $theme-gray-700;
}
......@@ -111,41 +114,24 @@
.node-detail-value-error {
color: $gl-danger;
}
}
.btn-show-advanced {
padding-left: 0;
border: 0;
}
.node-health-status {
display: inline-flex;
.status-text {
line-height: 18px;
}
}
.node-sync-settings {
display: inline-flex;
cursor: pointer;
.sync-status-icon {
margin-top: 2px;
fill: $theme-gray-700;
.section-items-container {
.node-detail-item {
&:first-child {
margin-top: 0;
}
}
.sync-status-event-info,
.event-status-timestamp {
color: $theme-gray-700;
}
.event-status-timestamp {
cursor: pointer;
}
}
}
.node-health-status,
.node-sync-settings,
.node-detail-section .btn-show-section {
display: flex;
align-items: center;
}
.geo-node-actions {
display: inline-flex;
justify-content: center;
......
---
title: Update Geo nodes layout for better usability
merge_request: 5199
author:
type: changed
......@@ -13,7 +13,7 @@ describe 'admin Geo Nodes', :js do
wait_for_requests
expect(page).to have_link('New node', href: new_admin_geo_node_path)
page.within(find('.geo-nodes', match: :first)) do
page.within(find('.geo-node-item', match: :first)) do
expect(page).to have_content(geo_node.url)
end
end
......@@ -33,7 +33,7 @@ describe 'admin Geo Nodes', :js do
expect(current_path).to eq admin_geo_nodes_path
wait_for_requests
page.within(find('.geo-nodes', match: :first)) do
page.within(find('.geo-node-item', match: :first)) do
expect(page).to have_content(geo_node.url)
end
end
......@@ -68,7 +68,7 @@ describe 'admin Geo Nodes', :js do
expect(current_path).to eq admin_geo_nodes_path
wait_for_requests
page.within(find('.geo-nodes', match: :first)) do
page.within(find('.geo-node-item', match: :first)) do
expect(page).to have_content('http://newsite.com')
expect(page).to have_content('Primary')
end
......@@ -91,7 +91,7 @@ describe 'admin Geo Nodes', :js do
expect(current_path).to eq admin_geo_nodes_path
wait_for_requests
expect(page).not_to have_css('.geo-nodes')
expect(page).not_to have_css('.geo-node-item')
end
end
end
......@@ -56,7 +56,6 @@ describe('AppComponent', () => {
expect(vm.modalKind).toBe('warning');
expect(vm.modalMessage).toBe('');
expect(vm.modalActionLabel).toBe('');
expect(vm.errorMessage).toBe('');
});
});
......@@ -91,10 +90,9 @@ describe('AppComponent', () => {
spyOn(vm.store, 'setNodes');
vm.fetchGeoNodes();
expect(vm.hasError).toBeFalsy();
setTimeout(() => {
expect(vm.store.setNodes).toHaveBeenCalledWith(mockNodes);
expect(vm.isLoading).toBeFalsy();
expect(vm.isLoading).toBe(false);
done();
}, 0);
});
......@@ -104,10 +102,9 @@ describe('AppComponent', () => {
statusCode = 500;
vm.fetchGeoNodes();
expect(vm.hasError).toBeFalsy();
setTimeout(() => {
expect(vm.hasError).toBeTruthy();
expect(vm.errorMessage.response.data).toBe(response);
expect(vm.isLoading).toBe(false);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while fetching nodes');
done();
}, 0);
});
......@@ -380,39 +377,13 @@ describe('AppComponent', () => {
});
describe('template', () => {
it('renders container elements correctly', () => {
expect(vm.$el.classList.contains('panel', 'panel-default')).toBeTruthy();
expect(vm.$el.querySelectorAll('.panel-heading').length).not.toBe(0);
expect(vm.$el.querySelector('.panel-heading').innerText.trim()).toBe('Geo nodes (0)');
it('renders container element with class `geo-nodes-container`', () => {
expect(vm.$el.classList.contains('geo-nodes-container')).toBe(true);
});
it('renders loading animation when `isLoading` is true', () => {
vm.isLoading = true;
expect(vm.$el.querySelectorAll('.loading-animation.prepend-top-20.append-bottom-20').length).not.toBe(0);
});
it('renders list of nodes', (done) => {
vm.store.setNodes(mockNodes);
vm.isLoading = false;
Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('.loading-animation.prepend-top-20.append-bottom-20').length).toBe(0);
expect(vm.$el.querySelectorAll('ul.geo-nodes').length).not.toBe(0);
done();
});
});
it('renders error message', (done) => {
vm.hasError = true;
vm.isLoading = false;
vm.errorMessage = 'Something went wrong.';
Vue.nextTick(() => {
const errEl = 'p.health-message.prepend-left-15.append-right-15';
expect(vm.$el.querySelectorAll(errEl).length).not.toBe(0);
expect(vm.$el.querySelector(errEl).innerText.trim()).toBe(vm.errorMessage);
done();
});
});
});
});
......@@ -46,16 +46,6 @@ describe('GeoNodeDetailItemComponent', () => {
vm.$destroy();
});
it('renders health status item value', () => {
const vm = createComponent({
itemValueType: VALUE_TYPE.CUSTOM,
customType: CUSTOM_TYPE.STATUS,
itemValue: rawMockNodeDetails.health,
});
expect(vm.$el.querySelectorAll('.node-health-status').length).not.toBe(0);
vm.$destroy();
});
it('renders sync settings item value', () => {
const vm = createComponent({
itemValueType: VALUE_TYPE.CUSTOM,
......
......@@ -2,14 +2,21 @@ import Vue from 'vue';
import geoNodeDetailsComponent from 'ee/geo_nodes/components/geo_node_details.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodes, mockNodeDetails } from '../mock_data';
const createComponent = (nodeDetails = mockNodeDetails) => {
import { mockNode, mockNodeDetails } from '../mock_data';
const createComponent = ({
node = mockNode,
nodeDetails = mockNodeDetails,
nodeActionsAllowed = true,
nodeEditAllowed = true,
}) => {
const Component = Vue.extend(geoNodeDetailsComponent);
return mountComponent(Component, {
node,
nodeDetails,
node: mockNodes[1],
nodeActionsAllowed,
nodeEditAllowed,
});
};
......@@ -17,7 +24,7 @@ describe('GeoNodeDetailsComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
vm = createComponent({});
});
afterEach(() => {
......@@ -28,7 +35,6 @@ describe('GeoNodeDetailsComponent', () => {
it('returns default data props', () => {
expect(vm.showAdvanceItems).toBeFalsy();
expect(vm.errorMessage).toBe('');
expect(Array.isArray(vm.nodeDetailItems)).toBeTruthy();
});
});
......@@ -40,7 +46,7 @@ describe('GeoNodeDetailsComponent', () => {
health: 'Something went wrong.',
healthy: false,
});
const vmX = createComponent(nodeDetails);
const vmX = createComponent({ nodeDetails });
expect(vmX.errorMessage).toBe('Something went wrong.');
expect(vmX.hasError).toBeTruthy();
vmX.$destroy();
......@@ -57,7 +63,7 @@ describe('GeoNodeDetailsComponent', () => {
primaryVersion: '10.3.0-pre',
primaryRevision: 'b93c51850b',
});
const vmX = createComponent(nodeDetails);
const vmX = createComponent({ nodeDetails });
expect(vmX.errorMessage).toBe('GitLab version does not match the primary node version');
expect(vmX.hasVersionMismatch).toBeTruthy();
vmX.$destroy();
......@@ -66,147 +72,11 @@ describe('GeoNodeDetailsComponent', () => {
expect(vm.hasVersionMismatch).toBeFalsy();
});
});
describe('advanceButtonIcon', () => {
it('returns button icon name', () => {
vm.showAdvanceItems = true;
expect(vm.advanceButtonIcon).toBe('angle-up');
vm.showAdvanceItems = false;
expect(vm.advanceButtonIcon).toBe('angle-down');
});
});
describe('nodeVersion', () => {
it('returns `Unknown` when `version` and `revision` are null', () => {
const nodeDetailsVersionNull = Object.assign({}, mockNodeDetails, {
version: null,
revision: null,
});
const vmVersionNull = createComponent(nodeDetailsVersionNull);
expect(vmVersionNull.nodeVersion).toBe('Unknown');
vmVersionNull.$destroy();
});
it('returns version string', () => {
expect(vm.nodeVersion).toBe('10.4.0-pre (b93c51849b)');
});
});
describe('replicationSlotWAL', () => {
it('returns replication slot WAL in Megabytes', () => {
expect(vm.replicationSlotWAL).toBe('479.37 MiB');
});
});
describe('dbReplicationLag', () => {
it('returns DB replication lag time duration', () => {
expect(vm.dbReplicationLag).toBe('0m');
});
it('returns `Unknown` when `dbReplicationLag` is null', () => {
const nodeDetailsLagNull = Object.assign({}, mockNodeDetails, {
dbReplicationLag: null,
});
const vmLagNull = createComponent(nodeDetailsLagNull);
expect(vmLagNull.dbReplicationLag).toBe('Unknown');
vmLagNull.$destroy();
});
});
describe('lastEventStatus', () => {
it('returns event status object', () => {
expect(vm.lastEventStatus.eventId).toBe(mockNodeDetails.lastEvent.id);
expect(vm.lastEventStatus.eventTimeStamp).toBe(mockNodeDetails.lastEvent.timeStamp);
});
});
describe('cursorLastEventStatus', () => {
it('returns event status object', () => {
expect(vm.cursorLastEventStatus.eventId).toBe(mockNodeDetails.cursorLastEvent.id);
expect(vm.cursorLastEventStatus.eventTimeStamp)
.toBe(mockNodeDetails.cursorLastEvent.timeStamp);
});
});
});
describe('methods', () => {
describe('nodeHealthStatus', () => {
it('returns health status string', () => {
// With altered mock data for Unhealthy status
const nodeDetails = Object.assign({}, mockNodeDetails, {
healthStatus: 'Unhealthy',
healthy: false,
});
const vmX = createComponent(nodeDetails);
expect(vmX.nodeHealthStatus()).toBe('Unhealthy');
vmX.$destroy();
// With default mock data
expect(vm.nodeHealthStatus()).toBe('Healthy');
});
});
describe('storageShardsStatus', () => {
it('returns storage shard status string', () => {
// With altered mock data for Unhealthy status
let nodeDetails = Object.assign({}, mockNodeDetails, {
storageShardsMatch: null,
});
let vmX = createComponent(nodeDetails);
expect(vmX.storageShardsStatus()).toBe('Unknown');
vmX.$destroy();
nodeDetails = Object.assign({}, mockNodeDetails, {
storageShardsMatch: true,
});
vmX = createComponent(nodeDetails);
expect(vmX.storageShardsStatus()).toBe('OK');
vmX.$destroy();
// With default mock data
expect(vm.storageShardsStatus()).toBe('Does not match the primary storage configuration');
});
});
describe('plainValueCssClass', () => {
it('returns CSS class for plain value item', () => {
expect(vm.plainValueCssClass()).toBe('node-detail-value-bold');
expect(vm.plainValueCssClass(true)).toBe('node-detail-value-bold node-detail-value-error');
});
});
describe('syncSettings', () => {
it('returns sync settings object', () => {
const nodeDetailsUnknownSync = Object.assign({}, mockNodeDetails, {
syncStatusUnavailable: true,
});
const vmUnknownSync = createComponent(nodeDetailsUnknownSync);
const syncSettings = vmUnknownSync.syncSettings();
expect(syncSettings.syncStatusUnavailable).toBe(true);
expect(syncSettings.namespaces).toBe(mockNodeDetails.namespaces);
expect(syncSettings.lastEvent).toBe(mockNodeDetails.lastEvent);
expect(syncSettings.cursorLastEvent).toBe(mockNodeDetails.cursorLastEvent);
vmUnknownSync.$destroy();
});
});
describe('onClickShowAdvance', () => {
it('toggles `showAdvanceItems` prop', () => {
vm.showAdvanceItems = true;
vm.onClickShowAdvance();
expect(vm.showAdvanceItems).toBeFalsy();
vm.showAdvanceItems = false;
vm.onClickShowAdvance();
expect(vm.showAdvanceItems).toBeTruthy();
});
});
});
describe('template', () => {
it('renders container elements correctly', () => {
expect(vm.$el.querySelectorAll('.node-details-list').length).not.toBe(0);
expect(vm.$el.querySelectorAll('.btn-show-advanced').length).not.toBe(0);
expect(vm.$el.classList.contains('panel-body')).toBe(true);
});
});
});
......@@ -4,15 +4,17 @@ import geoNodeEventStatusComponent from 'ee/geo_nodes/components/geo_node_event_
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../mock_data';
const createComponent = (
const createComponent = ({
eventId = mockNodeDetails.lastEvent.id,
eventTimeStamp = mockNodeDetails.lastEvent.timeStamp,
) => {
eventTypeLogStatus = false,
}) => {
const Component = Vue.extend(geoNodeEventStatusComponent);
return mountComponent(Component, {
eventId,
eventTimeStamp,
eventTypeLogStatus,
});
};
......@@ -20,7 +22,7 @@ describe('GeoNodeEventStatus', () => {
let vm;
beforeEach(() => {
vm = createComponent();
vm = createComponent({});
});
afterEach(() => {
......@@ -39,6 +41,18 @@ describe('GeoNodeEventStatus', () => {
expect(vm.timeStampString).toContain('Nov 21, 2017');
});
});
describe('eventString', () => {
it('returns computed event string when `eventTypeLogStatus` prop is true', () => {
const vmWithLogStatus = createComponent({ eventTypeLogStatus: true });
expect(vmWithLogStatus.eventString).toBe(`${mockNodeDetails.lastEvent.id} events behind`);
vmWithLogStatus.$destroy();
});
it('returns event ID as it is when `eventTypeLogStatus` prop is false', () => {
expect(vm.eventString).toBe(mockNodeDetails.lastEvent.id);
});
});
});
describe('template', () => {
......@@ -50,7 +64,10 @@ describe('GeoNodeEventStatus', () => {
});
it('renders empty state when timestamp is not present', () => {
const vmWithoutTimestamp = createComponent(0, 0);
const vmWithoutTimestamp = createComponent({
eventId: 0,
eventTimeStamp: 0,
});
expect(vmWithoutTimestamp.$el.querySelectorAll('strong').length).not.toBe(0);
expect(vmWithoutTimestamp.$el.querySelectorAll('.event-status-timestamp').length).toBe(0);
expect(vmWithoutTimestamp.$el.querySelector('strong').innerText.trim()).toBe('Not available');
......
import Vue from 'vue';
import GeoNodeHeaderComponent from 'ee/geo_nodes/components/geo_node_header.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNode, mockNodeDetails } from '../mock_data';
const createComponent = ({
node = Object.assign({}, mockNode),
nodeDetails = Object.assign({}, mockNodeDetails),
nodeDetailsLoading = false,
nodeDetailsFailed = false,
}) => {
const Component = Vue.extend(GeoNodeHeaderComponent);
return mountComponent(Component, {
node,
nodeDetails,
nodeDetailsLoading,
nodeDetailsFailed,
});
};
describe('GeoNodeHeader', () => {
let vm;
beforeEach(() => {
vm = createComponent({});
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('isNodeHTTP', () => {
it('returns `true` when Node URL protocol is non-HTTPS', () => {
expect(vm.isNodeHTTP).toBe(true);
});
it('returns `false` when Node URL protocol is HTTPS', (done) => {
vm.node.url = 'https://127.0.0.1:3001/';
Vue.nextTick()
.then(() => {
expect(vm.isNodeHTTP).toBe(false);
})
.then(done)
.catch(done.fail);
});
});
describe('showNodeStatusIcon', () => {
it('returns `false` when Node details are still loading', (done) => {
vm.nodeDetailsLoading = true;
Vue.nextTick()
.then(() => {
expect(vm.showNodeStatusIcon).toBe(false);
})
.then(done)
.catch(done.fail);
});
it('returns `true` when Node details failed to load', (done) => {
vm.nodeDetailsFailed = true;
Vue.nextTick()
.then(() => {
expect(vm.showNodeStatusIcon).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('returns `true` when Node details loaded and Node URL is non-HTTPS', (done) => {
vm.nodeDetailsLoading = false;
vm.nodeDetailsFailed = false;
vm.node.url = mockNode.url;
Vue.nextTick()
.then(() => {
expect(vm.showNodeStatusIcon).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('returns `false` when Node details loaded and Node URL is HTTPS', (done) => {
vm.node.url = 'https://127.0.0.1:3001/';
Vue.nextTick()
.then(() => {
expect(vm.showNodeStatusIcon).toBe(false);
})
.then(done)
.catch(done.fail);
});
});
});
});
......@@ -50,10 +50,13 @@ describe('GeoNodeHealthStatusComponent', () => {
describe('template', () => {
it('renders container elements correctly', () => {
const vm = createComponent('Healthy');
expect(vm.$el.classList.contains('node-detail-value', 'node-health-status', 'geo-node-healthy')).toBeTruthy();
expect(vm.$el.querySelectorAll('svg').length).not.toBe(0);
expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('#status_success');
expect(vm.$el.querySelector('.status-text').innerText.trim()).toBe('Healthy');
expect(vm.$el.classList.contains('detail-section-item')).toBe(true);
expect(vm.$el.querySelector('.node-detail-title').innerText.trim()).toBe('Health status:');
const iconContainerEl = vm.$el.querySelector('.node-detail-value.node-health-status');
expect(iconContainerEl).not.toBeNull();
expect(iconContainerEl.querySelector('svg use').getAttribute('xlink:href')).toContain('#status_success');
expect(iconContainerEl.querySelector('.status-text').innerText.trim()).toBe('Healthy');
vm.$destroy();
});
});
......
......@@ -3,9 +3,9 @@ import Vue from 'vue';
import geoNodeItemComponent from 'ee/geo_nodes/components/geo_node_item.vue';
import eventHub from 'ee/geo_nodes/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodes, mockNodeDetails } from '../mock_data';
import { mockNode, mockNodeDetails } from '../mock_data';
const createComponent = (node = mockNodes[0]) => {
const createComponent = (node = mockNode) => {
const Component = Vue.extend(geoNodeItemComponent);
return mountComponent(Component, {
......@@ -29,8 +29,8 @@ describe('GeoNodeItemComponent', () => {
describe('data', () => {
it('returns default data props', () => {
expect(vm.isNodeDetailsLoading).toBeTruthy();
expect(vm.isNodeDetailsFailed).toBeFalsy();
expect(vm.isNodeDetailsLoading).toBe(true);
expect(vm.isNodeDetailsFailed).toBe(false);
expect(vm.nodeHealthStatus).toBe('');
expect(vm.errorMessage).toBe('');
expect(typeof vm.nodeDetails).toBe('object');
......@@ -39,57 +39,20 @@ describe('GeoNodeItemComponent', () => {
describe('computed', () => {
let vmHttps;
let mockNode;
let httpsNode;
beforeEach(() => {
// Altered mock data for secure URL
mockNode = Object.assign({}, mockNodes[0], {
httpsNode = Object.assign({}, mockNode, {
url: 'https://127.0.0.1:3001/',
});
vmHttps = createComponent(mockNode);
vmHttps = createComponent(httpsNode);
});
afterEach(() => {
vmHttps.$destroy();
});
describe('isNodeNonHTTPS', () => {
it('returns `true` if Node URL protocol is non-HTTPS', () => {
// With default mock data
expect(vm.isNodeNonHTTPS).toBeTruthy();
});
it('returns `false` is Node URL protocol is HTTPS', () => {
// With altered mock data
expect(vmHttps.isNodeNonHTTPS).toBeFalsy();
});
});
describe('showNodeStatusIcon', () => {
it('returns `false` if Node details are still loading', () => {
vm.isNodeDetailsLoading = true;
expect(vm.showNodeStatusIcon).toBeFalsy();
});
it('returns `true` if Node details failed to load', () => {
vm.isNodeDetailsLoading = false;
vm.isNodeDetailsFailed = true;
expect(vm.showNodeStatusIcon).toBeTruthy();
});
it('returns `true` if Node details loaded and Node URL is non-HTTPS', () => {
vm.isNodeDetailsLoading = false;
vm.isNodeDetailsFailed = false;
expect(vm.showNodeStatusIcon).toBeTruthy();
});
it('returns `false` if Node details loaded and Node URL is HTTPS', () => {
vmHttps.isNodeDetailsLoading = false;
vmHttps.isNodeDetailsFailed = false;
expect(vmHttps.showNodeStatusIcon).toBeFalsy();
});
});
describe('showNodeDetails', () => {
it('returns `false` if Node details are still loading', () => {
vm.isNodeDetailsLoading = true;
......@@ -108,50 +71,17 @@ describe('GeoNodeItemComponent', () => {
expect(vm.showNodeDetails).toBeTruthy();
});
});
describe('nodeStatusIconClass', () => {
it('returns `node-status-icon-failure` along with default classes if Node details failed to load', () => {
vm.isNodeDetailsFailed = true;
expect(vm.nodeStatusIconClass).toBe('prepend-left-10 pull-left node-status-icon-failure');
});
it('returns `node-status-icon-warning` along with default classes if Node details loaded and Node URL is non-HTTPS', () => {
vm.isNodeDetailsFailed = false;
expect(vm.nodeStatusIconClass).toBe('prepend-left-10 pull-left node-status-icon-warning');
});
});
describe('nodeStatusIconName', () => {
it('returns `warning` if Node details loaded and Node URL is non-HTTPS', () => {
vm.isNodeDetailsFailed = false;
expect(vm.nodeStatusIconName).toBe('warning');
});
it('returns `status_failed_borderless` if Node details failed to load', () => {
vm.isNodeDetailsFailed = true;
expect(vm.nodeStatusIconName).toBe('status_failed_borderless');
});
});
describe('nodeStatusIconTooltip', () => {
it('returns empty string if Node details failed to load', () => {
vm.isNodeDetailsFailed = true;
expect(vm.nodeStatusIconTooltip).toBe('');
});
it('returns tooltip string if Node details loaded and Node URL is non-HTTPS', () => {
vm.isNodeDetailsFailed = false;
expect(vm.nodeStatusIconTooltip).toBe('You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS.');
});
});
});
describe('methods', () => {
describe('handleNodeDetails', () => {
it('intializes props based on provided `nodeDetails`', () => {
// With altered mock data with matching ID
const mockNode = Object.assign({}, mockNodes[1]);
const vmNodePrimary = createComponent(mockNode);
const mockNodeSecondary = Object.assign({}, mockNode, {
id: mockNodeDetails.id,
primary: false,
});
const vmNodePrimary = createComponent(mockNodeSecondary);
vmNodePrimary.handleNodeDetails(mockNodeDetails);
expect(vmNodePrimary.isNodeDetailsLoading).toBeFalsy();
......@@ -212,21 +142,8 @@ describe('GeoNodeItemComponent', () => {
});
describe('template', () => {
it('renders node URL', () => {
expect(vm.$el.querySelectorAll('.node-url').length).not.toBe(0);
});
it('renders node details loading animation', () => {
vm.isNodeDetailsLoading = true;
expect(vm.$el.querySelectorAll('.node-details-loading').length).not.toBe(0);
});
it('renders node badge `Current node`', () => {
expect(vm.$el.querySelectorAll('.node-badge.current-node').length).not.toBe(0);
});
it('renders node badge `Primary`', () => {
expect(vm.$el.querySelectorAll('.node-badge.primary-node').length).not.toBe(0);
it('renders container element', () => {
expect(vm.$el.classList.contains('panel', 'panel-default', 'geo-node-item')).toBe(true);
});
it('renders node error message', (done) => {
......
......@@ -18,7 +18,7 @@ describe('GeoNodesListComponent', () => {
describe('template', () => {
it('renders container element correctly', () => {
const vm = createComponent();
expect(vm.$el.classList.contains('well-list', 'geo-nodes')).toBeTruthy();
expect(vm.$el.classList.contains('panel', 'panel-default')).toBe(true);
vm.$destroy();
});
});
......
import Vue from 'vue';
import NodeDetailsSectionMainComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_main.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNode, mockNodeDetails } from '../../mock_data';
const createComponent = ({
node = Object.assign({}, mockNode),
nodeDetails = Object.assign({}, mockNodeDetails),
nodeActionsAllowed = true,
nodeEditAllowed = true,
versionMismatch = false,
}) => {
const Component = Vue.extend(NodeDetailsSectionMainComponent);
return mountComponent(Component, {
node,
nodeDetails,
nodeActionsAllowed,
nodeEditAllowed,
versionMismatch,
});
};
describe('NodeDetailsSectionMain', () => {
let vm;
beforeEach(() => {
vm = createComponent({});
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('nodeVersion', () => {
it('returns `Unknown` when `version` and `revision` are null', (done) => {
vm.nodeDetails.version = null;
vm.nodeDetails.revision = null;
Vue.nextTick()
.then(() => {
expect(vm.nodeVersion).toBe('Unknown');
})
.then(done)
.catch(done.fail);
});
it('returns version string', () => {
expect(vm.nodeVersion).toBe('10.4.0-pre (b93c51849b)');
});
});
describe('nodeHealthStatus', () => {
it('returns health status string', (done) => {
// With default mock data
expect(vm.nodeHealthStatus).toBe('Healthy');
// With altered mock data for Unhealthy status
vm.nodeDetails.healthStatus = 'Unhealthy';
vm.nodeDetails.healthy = false;
Vue.nextTick()
.then(() => {
expect(vm.nodeHealthStatus).toBe('Unhealthy');
})
.then(done)
.catch(done.fail);
});
});
});
describe('template', () => {
it('renders component container element', () => {
expect(vm.$el.classList.contains('primary-section')).toBe(true);
});
it('renders node version element', () => {
expect(vm.$el.querySelector('.node-detail-title').innerText.trim()).toBe('GitLab version:');
expect(vm.$el.querySelector('.node-detail-value').innerText.trim()).toBe('10.4.0-pre (b93c51849b)');
});
});
});
import Vue from 'vue';
import NodeDetailsSectionOtherComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_other.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../../mock_data';
const createComponent = (
nodeDetails = Object.assign({}, mockNodeDetails),
) => {
const Component = Vue.extend(NodeDetailsSectionOtherComponent);
return mountComponent(Component, {
nodeDetails,
});
};
describe('NodeDetailsSectionOther', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.showSectionItems).toBe(false);
});
});
describe('computed', () => {
describe('storageShardsStatus', () => {
it('returns `Unknown` when `nodeDetails.storageShardsMatch` is null', (done) => {
vm.nodeDetails.storageShardsMatch = null;
Vue.nextTick()
.then(() => {
expect(vm.storageShardsStatus).toBe('Unknown');
})
.then(done)
.catch(done.fail);
});
it('returns `OK` when `nodeDetails.storageShardsMatch` is true', (done) => {
vm.nodeDetails.storageShardsMatch = true;
Vue.nextTick()
.then(() => {
expect(vm.storageShardsStatus).toBe('OK');
})
.then(done)
.catch(done.fail);
});
it('returns storage shard status string when `nodeDetails.storageShardsMatch` is false', () => {
expect(vm.storageShardsStatus).toBe('Does not match the primary storage configuration');
});
});
describe('storageShardsCssClass', () => {
it('returns CSS class `node-detail-value-bold` when `nodeDetails.storageShardsMatch` is true', (done) => {
vm.nodeDetails.storageShardsMatch = true;
Vue.nextTick()
.then(() => {
expect(vm.storageShardsCssClass).toBe('node-detail-value-bold');
})
.then(done)
.catch(done.fail);
});
it('returns CSS class `node-detail-value-bold node-detail-value-error` when `nodeDetails.storageShardsMatch` is false', () => {
expect(vm.storageShardsCssClass).toBe('node-detail-value-bold node-detail-value-error');
});
});
});
describe('template', () => {
it('renders component container element', () => {
expect(vm.$el.classList.contains('other-section')).toBe(true);
});
it('renders show section button element', () => {
expect(vm.$el.querySelector('.btn-show-section')).not.toBeNull();
expect(vm.$el.querySelector('.btn-show-section > span').innerText.trim()).toBe('Other information');
});
it('renders section items container element', () => {
expect(vm.$el.querySelector('.section-items-container')).not.toBeNull();
});
});
});
import Vue from 'vue';
import NodeDetailsSectionSyncComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_sync.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../../mock_data';
const createComponent = (
nodeDetails = Object.assign({}, mockNodeDetails),
) => {
const Component = Vue.extend(NodeDetailsSectionSyncComponent);
return mountComponent(Component, {
nodeDetails,
});
};
describe('NodeDetailsSectionSync', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.showSectionItems).toBe(false);
expect(Array.isArray(vm.nodeDetailItems)).toBe(true);
expect(vm.nodeDetailItems.length > 0).toBe(true);
});
});
describe('methods', () => {
describe('syncSettings', () => {
it('returns sync settings object', (done) => {
vm.nodeDetails.syncStatusUnavailable = true;
Vue.nextTick()
.then(() => {
const syncSettings = vm.syncSettings();
expect(syncSettings.syncStatusUnavailable).toBe(true);
expect(syncSettings.namespaces).toBe(mockNodeDetails.namespaces);
expect(syncSettings.lastEvent).toBe(mockNodeDetails.lastEvent);
expect(syncSettings.cursorLastEvent).toBe(mockNodeDetails.cursorLastEvent);
})
.then(done)
.catch(done.fail);
});
});
describe('dbReplicationLag', () => {
it('returns DB replication lag time duration', () => {
expect(vm.dbReplicationLag()).toBe('0m');
});
it('returns `Unknown` when `dbReplicationLag` is null', (done) => {
vm.nodeDetails.dbReplicationLag = null;
Vue.nextTick()
.then(() => {
expect(vm.dbReplicationLag()).toBe('Unknown');
})
.then(done)
.catch(done.fail);
});
});
describe('lastEventStatus', () => {
it('returns event status object', () => {
expect(vm.lastEventStatus().eventId).toBe(mockNodeDetails.lastEvent.id);
expect(vm.lastEventStatus().eventTimeStamp).toBe(mockNodeDetails.lastEvent.timeStamp);
});
});
describe('cursorLastEventStatus', () => {
it('returns event status object', () => {
expect(vm.cursorLastEventStatus().eventId).toBe(mockNodeDetails.cursorLastEvent.id);
expect(vm.cursorLastEventStatus().eventTimeStamp)
.toBe(mockNodeDetails.cursorLastEvent.timeStamp);
});
});
});
describe('template', () => {
it('renders component container element', () => {
expect(vm.$el.classList.contains('sync-section')).toBe(true);
});
it('renders show section button element', () => {
expect(vm.$el.querySelector('.btn-show-section')).not.toBeNull();
expect(vm.$el.querySelector('.btn-show-section > span').innerText.trim()).toBe('Sync information');
});
it('renders section items container element', () => {
expect(vm.$el.querySelector('.section-items-container')).not.toBeNull();
});
});
});
import Vue from 'vue';
import NodeDetailsSectionVerificationComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_verification.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../../mock_data';
const createComponent = ({
nodeDetails = mockNodeDetails,
nodeTypePrimary = false,
}) => {
const Component = Vue.extend(NodeDetailsSectionVerificationComponent);
return mountComponent(Component, {
nodeDetails,
nodeTypePrimary,
});
};
describe('NodeDetailsSectionVerification', () => {
let vm;
beforeEach(() => {
vm = createComponent({});
});
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.showSectionItems).toBe(false);
expect(Array.isArray(vm.primaryNodeDetailItems)).toBe(true);
expect(Array.isArray(vm.secondaryNodeDetailItems)).toBe(true);
expect(vm.primaryNodeDetailItems.length > 0).toBe(true);
expect(vm.secondaryNodeDetailItems.length > 0).toBe(true);
});
});
describe('computed', () => {
describe('hasItemsToShow', () => {
it('returns `true` when `nodeTypePrimary` prop is true', (done) => {
vm.nodeTypePrimary = true;
Vue.nextTick()
.then(() => {
expect(vm.hasItemsToShow).toBe(true);
})
.then(done)
.catch(done.fail);
});
it('returns value of `nodeDetails.repositoryVerificationEnabled` when `nodeTypePrimary` prop is false', () => {
expect(vm.hasItemsToShow).toBe(mockNodeDetails.repositoryVerificationEnabled);
});
});
});
describe('template', () => {
it('renders component container element', () => {
expect(vm.$el.classList.contains('verification-section')).toBe(true);
});
it('renders section items container element', (done) => {
vm.showSectionItems = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.section-items-container')).not.toBeNull();
done();
});
});
});
});
import Vue from 'vue';
import SectionRevealButtonComponent from 'ee/geo_nodes/components/node_detail_sections/section_reveal_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = (buttonTitle = 'Foo button') => {
const Component = Vue.extend(SectionRevealButtonComponent);
return mountComponent(Component, {
buttonTitle,
});
};
describe('SectionRevealButton', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.toggleState).toBe(false);
});
});
describe('computed', () => {
it('return `angle-up` when toggleState prop is true', () => {
vm.toggleState = true;
expect(vm.toggleButtonIcon).toBe('angle-up');
});
it('return `angle-down` when toggleState prop is false', () => {
vm.toggleState = false;
expect(vm.toggleButtonIcon).toBe('angle-down');
});
});
describe('methods', () => {
describe('onClickButton', () => {
it('updates `toggleState` prop to toggle from previous value', () => {
vm.toggleState = true;
vm.onClickButton();
expect(vm.toggleState).toBe(false);
});
it('emits `toggleButton` event on component', () => {
spyOn(vm, '$emit');
vm.onClickButton();
expect(vm.$emit).toHaveBeenCalledWith('toggleButton', vm.toggleState);
});
});
});
describe('template', () => {
it('renders button element', () => {
expect(vm.$el.classList.contains('btn-show-section')).toBe(true);
expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('#angle-down');
expect(vm.$el.querySelector('span').innerText.trim()).toBe('Foo button');
});
});
});
......@@ -145,6 +145,7 @@ export const mockNodeDetails = {
replicationSlotWAL: 502658737,
missingOAuthApplication: false,
storageShardsMatch: false,
repositoryVerificationEnabled: true,
replicationSlots: {
totalCount: null,
successCount: null,
......@@ -175,6 +176,16 @@ export const mockNodeDetails = {
successCount: 0,
failureCount: 0,
},
verifiedRepositories: {
totalCount: 0,
successCount: 0,
failureCount: 0,
},
verifiedWikis: {
totalCount: 0,
successCount: 0,
failureCount: 0,
},
lastEvent: {
id: 3,
timeStamp: 1511255200,
......
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