UI: reorg replication (#28332)

* Add replication-overview-mode component + tests

* Move both primary view higher to template

* simplify replication-summary component

* remove replication-mode-summary

* Add jsdocs to replication-overview-mode

* fix overview-mode test

* fix page/mode-index test

* copyright

* address PR comments

* note to devs
This commit is contained in:
Chelsea Shaw 2024-09-11 09:19:33 -05:00 committed by GitHub
parent abdeda43ca
commit e1c56a300f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 282 additions and 297 deletions

View File

@ -1,16 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
.replication-description {
flex-shrink: 1;
.title {
margin-bottom: $spacing-8;
}
.detail-tags {
margin-bottom: $spacing-16;
}
}

View File

@ -91,7 +91,6 @@
@import './components/read-more';
@import './components/regex-validator';
@import './components/replication-dashboard';
@import './components/replication-mode-summary';
@import './components/replication-page';
@import './components/replication-summary';
@import './components/role-item';

View File

@ -1,68 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { service } from '@ember/service';
import { equal } from '@ember/object/computed';
import { get, computed } from '@ember/object';
import Component from '@ember/component';
import layout from '../templates/components/replication-mode-summary';
const replicationAttr = function (attr) {
return computed(`cluster.{dr,performance}.${attr}`, 'cluster', 'mode', function () {
const { mode, cluster } = this;
return get(cluster, `${mode}.${attr}`);
});
};
export default Component.extend({
layout,
version: service(),
router: service(),
namespace: service(),
classNameBindings: ['isMenu::box'],
attributeBindings: ['href', 'target'],
display: 'banner',
isMenu: equal('display', 'menu'),
href: computed(
'cluster.id',
'display',
'mode',
'replicationEnabled',
'version.hasPerfReplication',
function () {
const display = this.display;
const mode = this.mode;
if (mode === 'performance' && display === 'menu' && this.version.hasPerfReplication === false) {
return 'https://www.hashicorp.com/products/vault';
}
if (this.replicationEnabled || display === 'menu') {
return this.router.urlFor('vault.cluster.replication.mode.index', this.cluster.id, mode);
}
return null;
}
),
target: computed('isPerformance', 'version.hasPerfReplication', function () {
if (this.isPerformance && this.version.hasPerfReplication === false) {
return '_blank';
}
return null;
}),
internalLink: false,
isPerformance: equal('mode', 'performance'),
replicationEnabled: replicationAttr('replicationEnabled'),
replicationUnsupported: equal('cluster.mode', 'unsupported'),
replicationDisabled: replicationAttr('replicationDisabled'),
syncProgressPercent: replicationAttr('syncProgressPercent'),
syncProgress: replicationAttr('syncProgress'),
secondaryId: replicationAttr('secondaryId'),
modeForUrl: replicationAttr('modeForUrl'),
clusterIdDisplay: replicationAttr('clusterIdDisplay'),
mode: null,
cluster: null,
modeState: computed('cluster', 'mode', function () {
const { cluster, mode } = this;
const clusterState = cluster[mode].state;
return clusterState;
}),
});

View File

@ -1,122 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if this.isMenu}}
{{! this is the status menu }}
<div class="level is-mobile">
<div class="is-flex-grow-1">
{{#if this.replicationUnsupported}}
Unsupported
{{else if this.replicationEnabled}}
<div>
{{concat (if (eq this.mode "performance") "Performance " "Disaster Recovery ") (capitalize this.modeForUrl)}}
</div>
{{#if this.secondaryId}}
<small>
<code>
{{this.secondaryId}}
</code>
</small>
{{/if}}
<small>
<code>
{{this.clusterIdDisplay}}
</code>
</small>
{{else if (and (eq this.mode "performance") (not (has-feature "Performance Replication")))}}
Learn more
{{else if this.auth.currentToken}}
Enable
{{if (eq this.mode "performance") "Performance" "Disaster Recovery"}}
{{else}}
<span class="has-text-grey-light">
{{if (eq this.mode "performance") "Performance" "Disaster Recovery"}}
</span>
{{/if}}
</div>
<div class="level-right">
{{#if this.replicationEnabled}}
{{#if (cluster-states this.modeState)}}
<span class={{if (get (cluster-states this.modeState) "isOk") "has-text-success" "has-text-danger"}}>
<Icon @name={{get (cluster-states this.modeState) "glyph"}} />
</span>
{{else if this.syncProgress}}
<progress value={{this.syncProgressPercent}} max="100" class="progress is-small is-narrow is-info">
{{this.syncProgress.progress}}
of
{{this.syncProgress.total}}
keys
</progress>
{{/if}}
{{else}}
<Icon @name="minus-circle" aria-label="Replication not enabled" class="has-text-grey-light" />
{{/if}}
</div>
</div>
{{else}}
{{! this is the replication index page }}
<div class="level">
<div class="replication-description level-left">
<div>
{{#if (and (eq this.mode "performance") (not (has-feature "Performance Replication")))}}
<p>
Performance Replication is a feature of Vault Enterprise Premium.
<ExternalLink
@href="https://hashicorp.com/products/vault/trial?source=vaultui_Performance%20Replication"
class="link"
data-test-upgrade-link-performance
>
Upgrade
</ExternalLink>
</p>
{{else if (and (eq this.mode "dr") (not (has-feature "DR Replication")))}}
<p>
Disaster Recovery is a feature of Vault Enterprise Premium.
<ExternalLink
@href="https://hashicorp.com/products/vault/trial?source=vaultui_Performance%20Replication"
class="link"
data-test-upgrade-link-dr
>
Upgrade
</ExternalLink>
</p>
{{else if this.replicationEnabled}}
<h6 class="title is-6 is-uppercase">
Enabled
</h6>
<div class="detail-tags">
<span class="has-text-grey">
{{capitalize this.modeForUrl}}
</span>
{{#if this.secondaryId}}
<span class="tag is-light has-text-grey-dark">
<code>
{{this.secondaryId}}
</code>
</span>
{{/if}}
<span class="tag is-light has-text-grey-dark">
<code>
{{this.clusterIdDisplay}}
</code>
</span>
</div>
{{/if}}
<p class="help has-text-grey-dark">
{{replication-mode-description this.mode}}
</p>
</div>
</div>
<div class="level-right">
<Hds::Button
@route="mode.index"
@models={{array this.cluster.name this.mode}}
@color={{if this.replicationDisabled "primary" "secondary"}}
@text={{if this.replicationDisabled "Enable" "Details"}}
data-test-replication-details-link={{this.mode}}
/>
</div>
</div>
{{/if}}

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
export { default } from 'core/components/replication-mode-summary';

View File

@ -0,0 +1,49 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
~}}
<Hds::Card::Container @level="mid" @hasBorder={{true}} ...attributes>
<div class="has-padding-m">
<div class="flex">
<Icon @size="24" @name={{this.details.icon}} />
<Hds::Text::Display
@tag="h2"
@size="400"
data-test-overview-mode-title
>{{this.details.blockTitle}}</Hds::Text::Display>
</div>
<div class="has-top-padding-m has-bottom-padding-m" data-test-overview-mode-body>
{{#if (not (has-feature this.details.feature))}}
<Hds::Text::Body @size="300" @tag="div">
{{this.details.upgradeTitle}}
<Hds::Link::Inline @href={{this.details.upgradeLink}} data-test-upgrade-link={{@mode}}>
Upgrade
</Hds::Link::Inline>
</Hds::Text::Body>
{{else if @model.replicationEnabled}}
<Hds::Text::Body @tag="div" @size="300" @weight="semibold" @color="strong">ENABLED</Hds::Text::Body>
<div class="has-bottom-padding-s">
<Hds::Text::Body @color="faint">{{capitalize @model.modeForUrl}}</Hds::Text::Body>
{{#if @model.secondaryId}}
<Hds::Badge @text={{@model.secondaryId}} />
{{/if}}
{{#if @model.clusterIdDisplay}}
<Hds::Badge @text={{@model.clusterIdDisplay}} />
{{/if}}
</div>
{{/if}}
<Hds::Text::Body @color="faint">{{replication-mode-description @mode}}</Hds::Text::Body>
</div>
{{#if (has-feature this.details.feature)}}
<Hds::Button
@route="mode.index"
@models={{array @clusterName @mode}}
@color={{if @model.replicationEnabled "secondary" "primary"}}
@text={{if @model.replicationEnabled "Details" "Enable"}}
data-test-replication-details-link={{@mode}}
/>
{{/if}}
</div>
</Hds::Card::Container>

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
/**
* @module ReplicationOverviewModeComponent
* ReplicationOverviewMode components are used on the Replication index page to display
* details about a given mode (DR or Performance) status.
*
* @example
* <ReplicationOverviewModeComponent @mode="dr" @model={{this.cluster.dr}} @clusterName={{this.cluster.name}} />
*
* @param {string} mode - should be "dr" or "performance"
* @param {ReplicationAttributesModel} model - either the dr or performance attribute of the cluster model
* @param {string} clusterName - used for the link to the mode details
*/
export default class ReplicationOverviewModeComponent extends Component {
get details() {
if (this.args.mode === 'dr') {
return {
blockTitle: 'Disaster Recovery (DR)',
upgradeTitle: 'Disaster Recovery is a feature of Vault Enterprise Premium.',
upgradeLink: 'https://hashicorp.com/products/vault/trial?source=vaultui_DR%20Replication',
feature: 'DR Replication',
icon: 'replication-direct',
};
}
return {
blockTitle: 'Performance',
upgradeTitle: 'Performance Replication is a feature of Vault Enterprise Premium.',
upgradeLink: 'https://hashicorp.com/products/vault/trial?source=vaultui_Performance%20Replication',
feature: 'Performance Replication',
icon: 'replication-perf',
};
}
}

View File

@ -8,6 +8,13 @@ import { computed } from '@ember/object';
import Component from '@ember/component';
import ReplicationActions from 'core/mixins/replication-actions';
/**
* @module ReplicationSummary
* ReplicationSummary component is a component to show the mode-specific summary for replication
*
* @param {ClusterModel} cluster - the cluster ember-data model
* @param {string} initialReplicationMode - mode for replication details we want to see, either "dr" or "performance"
*/
export default Component.extend(ReplicationActions, {
'data-test-replication-summary': true,
attributeBindings: ['data-test-replication-summary'],
@ -22,7 +29,6 @@ export default Component.extend(ReplicationActions, {
this.set('replicationMode', initialReplicationMode);
}
},
showModeSummary: false,
initialReplicationMode: null,
cluster: null,

View File

@ -3,88 +3,39 @@
SPDX-License-Identifier: BUSL-1.1
~}}
{{#if (not (has-feature "DR Replication"))}}
<UpgradePage @title="Replication" />
{{else if this.showModeSummary}}
{{#if (not (and this.cluster.dr.replicationEnabled this.cluster.performance.replicationEnabled))}}
<PageHeader as |p|>
<p.levelLeft>
<h1 class="title is-3" data-test-replication-title>
Replication
</h1>
</p.levelLeft>
</PageHeader>
{{/if}}
{{#if (and (eq this.cluster.dr.mode "primary") (eq this.cluster.performance.mode "primary"))}}
{{#if (eq this.attrsForCurrentMode.mode "initializing")}}
The cluster is initializing replication. This may take some time.
{{else}}
<p>{{this.cluster.replicationModeStatus.cluster_id}}</p>
<div class="replication">
<ReplicationPage @model={{this.cluster}} as |Page|>
<Page.header @showTabs={{true}} />
<Page.dashboard @componentToRender="replication-summary-card" as |Dashboard|>
<Dashboard.card @title="Disaster Recovery" />
<Dashboard.card @title="Performance" />
<Page.dashboard
@data={{this.cluster}}
@componentToRender={{if
(eq this.attrsForCurrentMode.mode "secondary")
"replication-secondary-card"
"replication-primary-card"
}}
as |Dashboard|
>
{{#if (eq this.attrsForCurrentMode.mode "secondary")}}
<Dashboard.card @title="Status" />
<Dashboard.card @title="Primary cluster" />
{{else}}
<Dashboard.card
@title="State"
@description="The clusters current operating state."
@glyph={{get (cluster-states this.attrsForCurrentMode.state) "glyph"}}
@metric={{this.attrsForCurrentMode.state}}
/>
<Dashboard.card
@title="Last WAL entry"
@description="Index of last Write Ahead Logs entry written on local storage. Updates every ten seconds."
@metric={{format-number this.attrsForCurrentMode.lastWAL}}
/>
<Dashboard.secondaryCard @cluster={{this.cluster}} @replicationAttrs={{this.attrsForCurrentMode}} />
{{/if}}
</Page.dashboard>
</ReplicationPage>
{{else}}
<div class="box is-sideless is-fullwidth is-marginless">
<h3 class="title is-flex-center is-5 is-marginless">
<Icon @size="24" @name="replication-direct" />
Disaster Recovery (DR)
</h3>
{{#if this.cluster.dr.replicationEnabled}}
{{#if this.submit.isRunning}}
<LayoutLoading />
{{else}}
<ReplicationModeSummary @mode="dr" @cluster={{this.cluster}} @tagName="span" />
{{/if}}
{{else}}
<ReplicationModeSummary @mode="dr" @cluster={{this.cluster}} @tagName="div" />
{{/if}}
</div>
{{#if (not (and this.submit.isRunning (eq this.cluster.dr.mode "bootstrapping")))}}
<div class="box is-bottomless is-fullwidth is-marginless">
<h3 class="title is-flex-center is-5 is-marginless">
<Icon @size="24" @name="replication-perf" />
Performance
</h3>
<ReplicationModeSummary @mode="performance" @cluster={{this.cluster}} @tagName="span" />
</div>
{{/if}}
{{/if}}
{{else}}
{{#if (eq this.attrsForCurrentMode.mode "initializing")}}
The cluster is initializing replication. This may take some time.
{{else}}
<p>{{this.cluster.replicationModeStatus.cluster_id}}</p>
<div class="replication">
<ReplicationPage @model={{this.cluster}} as |Page|>
<Page.dashboard
@data={{this.cluster}}
@componentToRender={{if
(eq this.attrsForCurrentMode.mode "secondary")
"replication-secondary-card"
"replication-primary-card"
}}
as |Dashboard|
>
{{#if (eq this.attrsForCurrentMode.mode "secondary")}}
<Dashboard.card @title="Status" />
<Dashboard.card @title="Primary cluster" />
{{else}}
<Dashboard.card
@title="State"
@description="The clusters current operating state."
@glyph={{get (cluster-states this.attrsForCurrentMode.state) "glyph"}}
@metric={{this.attrsForCurrentMode.state}}
/>
<Dashboard.card
@title="Last WAL entry"
@description="Index of last Write Ahead Logs entry written on local storage. Updates every ten seconds."
@metric={{format-number this.attrsForCurrentMode.lastWAL}}
/>
<Dashboard.secondaryCard @cluster={{this.cluster}} @replicationAttrs={{this.attrsForCurrentMode}} />
{{/if}}
</Page.dashboard>
</ReplicationPage>
</div>
{{/if}}
</div>
{{/if}}

View File

@ -6,6 +6,7 @@
<section class="section">
<div class="container is-widescreen">
{{#if (eq this.model.mode "unsupported")}}
{{! Replication is unsupported in non-enterprise or when using non-transactional storage (eg inmem) }}
<PageHeader as |p|>
<p.levelLeft>
<h1 class="title is-3 has-text-grey" data-test-replication-title>
@ -98,8 +99,36 @@
@onSuccess={{this.onEnableSuccess}}
@doTransition={{true}}
/>
{{else if (not (has-feature "DR Replication"))}}
<UpgradePage @title="Replication" />
{{else if (and (eq this.model.dr.mode "primary") (eq this.model.performance.mode "primary"))}}
{{! Renders when cluster is primary for both replication modes }}
<ReplicationPage @model={{this.model}} as |Page|>
<Page.header @showTabs={{true}} />
<Page.dashboard @componentToRender="replication-summary-card" as |Dashboard|>
<Dashboard.card @title="Disaster Recovery" />
<Dashboard.card @title="Performance" />
</Page.dashboard>
</ReplicationPage>
{{else}}
<ReplicationSummary @cluster={{this.model}} @showModeSummary={{true}} @onDisable={{this.onDisable}} />
{{! Renders when at least one mode is not enabled }}
<PageHeader as |p|>
<p.levelLeft>
<h1 class="title is-3" data-test-replication-title>
Replication
</h1>
</p.levelLeft>
</PageHeader>
<div class="box is-sideless is-fullwidth is-marginless flex-col">
<ReplicationOverviewMode
@mode="dr"
@model={{this.model.dr}}
@clusterName={{this.model.name}}
class="has-bottom-margin-m"
/>
<ReplicationOverviewMode @mode="performance" @model={{this.model.performance}} @clusterName={{this.model.name}} />
</div>
{{/if}}
</div>
</section>

View File

@ -22,7 +22,7 @@ module('Integration | Component | replication page/mode-index', function (hooks)
hooks.beforeEach(function () {
this.store = this.owner.lookup('service:store');
this.onEnable = () => {};
this.clusterModel = {};
this.clusterModel = { replicationAttrs: {} };
this.replicationMode = '';
this.replicationDisabled = true;

View File

@ -0,0 +1,124 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render, settled } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupEngine } from 'ember-engines/test-support';
const OVERVIEW_MODE = {
title: '[data-test-overview-mode-title]',
body: '[data-test-overview-mode-body]',
detailsLink: '[data-test-replication-details-link]',
};
module('Integration | Component | replication-overview-mode', function (hooks) {
setupRenderingTest(hooks);
setupEngine(hooks, 'replication');
hooks.beforeEach(function () {
this.versionService = this.owner.lookup('service:version');
this.versionService.features = [];
this.mode = 'dr';
this.clusterName = 'foobar';
this.modeDetails = { mode: 'disabled' };
this.renderComponent = async () => {
return render(
hbs`
<ReplicationOverviewMode
@clusterName={{this.clusterName}}
@mode={{this.mode}}
@model={{this.modeDetails}}
/>`,
{ owner: this.engine }
);
};
});
test('without features', async function (assert) {
await this.renderComponent();
assert.dom(OVERVIEW_MODE.title).hasText('Disaster Recovery (DR)');
assert
.dom(OVERVIEW_MODE.body)
.includesText('Disaster Recovery is a feature of Vault Enterprise Premium. Upgrade');
assert.dom(OVERVIEW_MODE.detailsLink).doesNotExist('does not show link to replication (dr)');
this.set('mode', 'performance');
await settled();
assert.dom(OVERVIEW_MODE.title).hasText('Performance');
assert
.dom(OVERVIEW_MODE.body)
.includesText('Performance Replication is a feature of Vault Enterprise Premium. Upgrade');
assert.dom(OVERVIEW_MODE.detailsLink).doesNotExist('does not show link to replication (perf)');
});
module('with features', function (hooks) {
hooks.beforeEach(function () {
this.versionService.features = ['DR Replication', 'Performance Replication'];
});
test('it renders when replication disabled', async function (assert) {
await this.renderComponent();
assert.dom(OVERVIEW_MODE.title).hasText('Disaster Recovery (DR)');
assert
.dom(OVERVIEW_MODE.body)
.hasText(
'Disaster Recovery Replication is designed to protect against catastrophic failure of entire clusters. Secondaries do not forward service requests until they are elected and become a new primary.'
);
assert.dom(OVERVIEW_MODE.detailsLink).hasText('Enable');
this.set('mode', 'performance');
await settled();
assert.dom(OVERVIEW_MODE.title).hasText('Performance');
assert
.dom(OVERVIEW_MODE.body)
.hasText(
'Performance Replication scales workloads horizontally across clusters to make requests faster. Local secondaries handle read requests but forward writes to the primary to be handled.'
);
assert.dom(OVERVIEW_MODE.detailsLink).hasText('Enable');
});
test('it renders when replication enabled', async function (assert) {
this.mode = 'performance';
this.modeDetails = {
replicationEnabled: true,
mode: 'primary',
modeForUrl: 'primary',
clusterIdDisplay: 'foobar12',
};
await this.renderComponent();
assert.dom(OVERVIEW_MODE.title).hasText('Performance');
assert
.dom(OVERVIEW_MODE.body)
.includesText('ENABLED Primary foobar12', 'renders mode type and cluster ID if passed');
assert.dom(OVERVIEW_MODE.detailsLink).hasText('Details');
this.set('modeDetails', {
replicationEnabled: true,
mode: 'secondary',
modeForUrl: 'secondary',
clusterIdDisplay: 'foobar12',
secondaryId: 'some-secondary',
});
await settled();
assert.dom(OVERVIEW_MODE.title).hasText('Performance');
assert.dom(OVERVIEW_MODE.body).includesText('ENABLED Secondary some-secondary foobar12');
assert.dom(OVERVIEW_MODE.detailsLink).hasText('Details');
});
test('it renders when replication bootstrapping', async function (assert) {
this.modeDetails = {
replicationEnabled: true,
mode: 'bootstrapping',
modeForUrl: 'bootstrapping',
};
await this.renderComponent();
assert.dom(OVERVIEW_MODE.title).hasText('Disaster Recovery (DR)');
assert.dom(OVERVIEW_MODE.body).includesText('ENABLED Bootstrapping');
assert.dom(OVERVIEW_MODE.detailsLink).hasText('Details');
});
});
});