UI: glimmerize replication page (#25838)

This commit is contained in:
Chelsea Shaw
2024-03-11 16:22:36 -05:00
committed by GitHub
parent a26d04c77b
commit 425b6279b5
14 changed files with 173 additions and 190 deletions

View File

@@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<header class="page-header">
<header class="page-header" ...attributes>
{{#if this.hasLevel}}
{{yield (hash top=(component "page-header-level"))}}
<div class="level">

View File

@@ -22,7 +22,7 @@
</Hds::Alert>
</div>
{{/if}}
{{#if this.isSummaryDashboard}}
{{#if @isSummaryDashboard}}
<div class="summary-state">
<h6 class="title is-5 {{if (not (get (cluster-states this.summaryState) 'isOk')) 'has-text-danger'}}" data-test-error>
state
@@ -47,15 +47,15 @@
</h2>
</div>
<div class="selectable-card-container summary" data-test-selectable-card-container-summary>
{{yield (hash card=(component this.componentToRender replicationDetails=this.replicationDetailsSummary))}}
{{yield (hash card=(component @componentToRender replicationDetails=@replicationDetailsSummary))}}
</div>
{{else}}
<div
class="selectable-card-container {{if this.isSecondary 'secondary' 'primary'}}"
data-test-selectable-card-container={{if this.isSecondary "secondary" "primary"}}
class="selectable-card-container {{if @isSecondary 'secondary' 'primary'}}"
data-test-selectable-card-container={{if @isSecondary "secondary" "primary"}}
>
{{yield (hash card=(component this.componentToRender replicationDetails=this.replicationDetails))}}
{{#unless this.isSecondary}}
{{yield (hash card=(component @componentToRender replicationDetails=@replicationDetails))}}
{{#unless @isSecondary}}
{{yield (hash secondaryCard=(component "known-secondaries-card"))}}
{{/unless}}
</div>
@@ -71,8 +71,8 @@
</Hds::Alert>
</div>
{{/if}}
{{#unless this.isSummaryDashboard}}
<ReplicationTableRows @replicationDetails={{this.replicationDetails}} @clusterMode={{this.clusterMode}} />
{{#unless @isSummaryDashboard}}
<ReplicationTableRows @replicationDetails={{@replicationDetails}} @clusterMode={{@clusterMode}} />
<div class="replication helper-text float-right" data-test-replication-doc-link>
<p class="has-text-grey">
We have additional time series telemetry that can be found

View File

@@ -3,11 +3,9 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
import { computed } from '@ember/object';
import Component from '@glimmer/component';
import { clusterStates } from 'core/helpers/cluster-states';
import { capitalize } from '@ember/string';
import layout from '../templates/components/replication-dashboard';
/**
* @module ReplicationDashboard
@@ -18,7 +16,6 @@ import layout from '../templates/components/replication-dashboard';
* @example
* ```js
* <ReplicationDashboard
@data={{model}}
@componentToRender='replication-primary-card'
@isSecondary=false
@isSummaryDashboard=false
@@ -28,7 +25,6 @@ import layout from '../templates/components/replication-dashboard';
@reindexingDetails={{reindexingDetails}}
/>
* ```
* @param {Object} data=null - An Ember data object that is pulled from the Ember Cluster Model.
* @param {String} [componentToRender=''] - A string that determines which card component is displayed. There are three options, replication-primary-card, replication-secondary-card, replication-summary-card.
* @param {Boolean} [isSecondary=false] - Used to determine the title and display logic.
* @param {Boolean} [isSummaryDashboard=false] - Only true when the cluster is both a dr and performance primary. If true, replicationDetailsSummary is populated and used to pass through the cluster details.
@@ -38,34 +34,25 @@ import layout from '../templates/components/replication-dashboard';
* @param {Object} reindexingDetails=null - An Ember data object used to show a reindexing progress bar.
*/
export default Component.extend({
layout,
componentToRender: '',
data: null,
isSecondary: false,
isSummaryDashboard: false,
replicationDetails: null,
replicationDetailsSummary: null,
isSyncing: computed('replicationDetails.state', 'isSecondary', function () {
const { state } = this.replicationDetails;
const isSecondary = this.isSecondary;
export default class ReplicationDashboard extends Component {
get isSyncing() {
const { state } = this.args.replicationDetails;
const isSecondary = this.args.isSecondary;
return isSecondary && state && clusterStates([state]).isSyncing;
}),
isReindexing: computed('replicationDetails.reindex_in_progress', function () {
const { replicationDetails } = this;
return !!replicationDetails.reindex_in_progress;
}),
reindexingStage: computed('replicationDetails.reindex_stage', function () {
const { replicationDetails } = this;
const stage = replicationDetails.reindex_stage;
}
get isReindexing() {
return !!this.args.replicationDetails.reindex_in_progress;
}
get reindexingStage() {
const stage = this.args.replicationDetails.reindex_stage;
// specify the stage if we have one
if (stage) {
return `: ${capitalize(stage)}`;
}
return '';
}),
progressBar: computed('replicationDetails.{reindex_building_progress,reindex_building_total}', function () {
const { reindex_building_progress, reindex_building_total } = this.replicationDetails;
}
get progressBar() {
const { reindex_building_progress, reindex_building_total } = this.args.replicationDetails;
let progressBar = null;
if (reindex_building_progress && reindex_building_total) {
@@ -76,9 +63,9 @@ export default Component.extend({
}
return progressBar;
}),
summaryState: computed('replicationDetailsSummary.{dr.state,performance.state}', function () {
const { replicationDetailsSummary } = this;
}
get summaryState() {
const { replicationDetailsSummary } = this.args;
const drState = replicationDetailsSummary.dr.state;
const performanceState = replicationDetailsSummary.performance.state;
@@ -90,11 +77,11 @@ export default Component.extend({
}
return drState;
}),
reindexMessage: computed('isSecondary', 'progressBar', function () {
if (!this.isSecondary) {
}
get reindexMessage() {
if (!this.args.isSecondary) {
return 'This can cause a delay depending on the size of the data store. You can <b>not</b> use Vault during this time.';
}
return 'This can cause a delay depending on the size of the data store. You can use Vault during this time.';
}),
});
}
}

View File

@@ -3,34 +3,34 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<PageHeader as |p|>
<PageHeader data-test-replication-header as |p|>
<p.top>
{{#if (not (or this.isSummaryDashboard this.isSecondary))}}
{{#if (not (or @isSummaryDashboard @isSecondary))}}
<Hds::Breadcrumb>
<Hds::Breadcrumb::Item @text="Replication" @route="vault.cluster.replication.index" />
<Hds::Breadcrumb::Item @text={{this.title}} @current={{true}} />
<Hds::Breadcrumb::Item @text={{@title}} @current={{true}} />
</Hds::Breadcrumb>
{{/if}}
</p.top>
<p.levelLeft>
<h1 class="title is-3" data-test-replication-title>
{{this.title}}
{{#if this.data.anyReplicationEnabled}}
{{@title}}
{{#if @data.anyReplicationEnabled}}
<span class="tag is-light has-text-grey-dark" data-test-mode>
{{if this.isSecondary "secondary" "primary"}}
{{if @isSecondary "secondary" "primary"}}
</span>
<span class="tag is-light has-text-grey-dark" data-test-secondaryId>
{{this.secondaryId}}
{{@secondaryId}}
</span>
{{/if}}
</h1>
</p.levelLeft>
</PageHeader>
{{#if this.showTabs}}
{{#if @showTabs}}
<div class="tabs-container box is-bottomless is-fullwidth is-paddingless has-bottom-margin-l" data-test-tabs>
<nav class="tabs">
{{#if this.isSummaryDashboard}}
{{#if @isSummaryDashboard}}
<ul>
<li class="is-active">
<LinkToExternal @route="replication">Summary</LinkToExternal>
@@ -38,12 +38,16 @@
</ul>
{{else}}
<ul>
<li>
<LinkTo @route="vault.cluster.replication-dr-promote.details">
Details
</LinkTo>
</li>
<li>
<LinkTo @route="vault.cluster.replication-dr-promote" @current-when="vault.cluster.replication-dr-promote.index">
Manage
</LinkTo>
</li>
</ul>
{{/if}}
</nav>

View File

@@ -1,37 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
import layout from '../templates/components/replication-header';
/**
* @module ReplicationHeader
* The `ReplicationHeader` is a header component used on the Replication Dashboards.
*
* @example
* ```js
* <ReplicationHeader
@data={{model}}
@title="Secondary"
@secondaryID="meep_123"
@isSummaryDashboard=false
/>
* ```
* @param {Object} model=null - An Ember data object pulled from the Ember cluster model.
* @param {String} title=null - The title of the header.
* @param {String} [secondaryID=null] - The secondaryID pulled off of the model object.
* @param {Boolean} isSummaryDashboard=false - True when you have both a primary performance and dr cluster dashboard.
*/
export default Component.extend({
layout,
data: null,
classNames: ['replication-header'],
isSecondary: null,
secondaryId: null,
isSummaryDashboard: false,
'data-test-replication-header': true,
attributeBindings: ['data-test-replication-header'],
});

View File

@@ -3,7 +3,12 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<div class="replication-page" data-test-replication-page>
<div
class="replication-page"
{{did-insert this.onModeUpdate @model.replicationMode}}
{{did-update this.onModeUpdate @model.replicationMode}}
data-test-replication-page
>
{{#if this.isLoadingData}}
<LayoutLoading />
{{else}}
@@ -11,7 +16,7 @@
(hash
header=(component
"replication-header"
data=this.model
data=@model
title=this.formattedReplicationMode
isSecondary=this.isSecondary
secondaryId=this.replicationDetails.secondaryId
@@ -20,7 +25,6 @@
)
dashboard=(component
"replication-dashboard"
data=this.model
isSecondary=this.isSecondary
isSummaryDashboard=this.isSummaryDashboard
replicationDetailsSummary=this.replicationDetailsSummary

View File

@@ -3,11 +3,12 @@
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
import { computed } from '@ember/object';
import layout from '../templates/components/replication-page';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
/**
* @module ReplicationPage
@@ -20,7 +21,7 @@ import { task } from 'ember-concurrency';
@model={{cluster}}
/>
* ```
* @param {Object} cluster=null - An Ember data object that is pulled from the Ember Cluster Model.
* @param {Object} model=null - An Ember data object that is pulled from the Ember Cluster Model.
*/
const MODE = {
@@ -28,19 +29,20 @@ const MODE = {
performance: 'Performance',
};
export default Component.extend({
layout,
store: service(),
router: service(),
reindexingDetails: null,
didReceiveAttrs() {
this._super(arguments);
this.getReplicationModeStatus.perform();
},
getReplicationModeStatus: task(function* () {
let resp;
const { replicationMode } = this.model;
export default class ReplicationPage extends Component {
@service store;
@service router;
@tracked reindexingDetails = null;
@action onModeUpdate(evt, replicationMode) {
// Called on did-insert and did-update
this.getReplicationModeStatus.perform(replicationMode);
}
@task
@waitFor
*getReplicationModeStatus(replicationMode) {
let resp;
if (this.isSummaryDashboard) {
// the summary dashboard is not mode specific and will error
// while running replication/null/status in the replication-mode adapter
@@ -51,97 +53,82 @@ export default Component.extend({
resp = yield this.store.adapterFor('replication-mode').fetchStatus(replicationMode);
} catch (e) {
// do not handle error
} finally {
this.reindexingDetails = resp;
}
this.set('reindexingDetails', resp);
}),
isSummaryDashboard: computed('model.{performance.mode,dr.mode}', function () {
const router = this.router;
const currentRoute = router.get('currentRouteName');
}
get isSummaryDashboard() {
const currentRoute = this.router.currentRouteName;
// we only show the summary dashboard in the replication index route
if (currentRoute === 'vault.cluster.replication.index') {
const drMode = this.model.dr.mode;
const performanceMode = this.model.performance.mode;
const drMode = this.args.model.dr.mode;
const performanceMode = this.args.model.performance.mode;
return drMode === 'primary' && performanceMode === 'primary';
}
return '';
}),
formattedReplicationMode: computed('model.replicationMode', 'isSummaryDashboard', function () {
}
get formattedReplicationMode() {
// dr or performance 🤯
const { isSummaryDashboard } = this;
if (isSummaryDashboard) {
if (this.isSummaryDashboard) {
return 'Disaster Recovery & Performance';
}
const mode = this.model.replicationMode;
const mode = this.args.model.replicationMode;
return MODE[mode];
}),
clusterMode: computed('model.replicationAttrs', 'isSummaryDashboard', function () {
}
get clusterMode() {
// primary or secondary
const { model } = this;
const { isSummaryDashboard } = this;
if (isSummaryDashboard) {
if (this.isSummaryDashboard) {
// replicationAttrs does not exist when summaryDashboard
return 'primary';
}
return model.replicationAttrs.mode;
}),
isLoadingData: computed('clusterMode', 'model.replicationAttrs', function () {
const { clusterMode } = this;
const { model } = this;
const { isSummaryDashboard } = this;
if (isSummaryDashboard) {
return this.args.model.replicationAttrs.mode;
}
get isLoadingData() {
if (this.isSummaryDashboard) {
return false;
}
const clusterId = model.replicationAttrs.clusterId;
const replicationDisabled = model.replicationAttrs.replicationDisabled;
if (clusterMode === 'bootstrapping' || (!clusterId && !replicationDisabled)) {
const { clusterId, replicationDisabled } = this.args.model.replicationAttrs;
if (this.clusterMode === 'bootstrapping' || (!clusterId && !replicationDisabled)) {
// if clusterMode is bootstrapping
// if no clusterId, the data hasn't loaded yet, wait for another status endpoint to be called
return true;
}
return false;
}),
isSecondary: computed('clusterMode', function () {
const { clusterMode } = this;
return clusterMode === 'secondary';
}),
replicationDetailsSummary: computed('isSummaryDashboard', function () {
const { model } = this;
const { isSummaryDashboard } = this;
if (!isSummaryDashboard) {
return;
}
if (isSummaryDashboard) {
get isSecondary() {
return this.clusterMode === 'secondary';
}
get replicationDetailsSummary() {
if (this.isSummaryDashboard) {
const combinedObject = {};
combinedObject.dr = model['dr'];
combinedObject.performance = model['performance'];
combinedObject.dr = this.args.model['dr'];
combinedObject.performance = this.args.model['performance'];
return combinedObject;
}
return {};
}),
replicationDetails: computed('model.replicationMode', 'isSummaryDashboard', function () {
const { model } = this;
const { isSummaryDashboard } = this;
if (isSummaryDashboard) {
}
get replicationDetails() {
if (this.isSummaryDashboard) {
// Cannot return null
return {};
}
const replicationMode = model.replicationMode;
return model[replicationMode];
}),
isDisabled: computed('replicationDetails.mode', function () {
const { replicationMode } = this.args.model;
return this.args.model[replicationMode];
}
get isDisabled() {
if (this.replicationDetails.mode === 'disabled' || this.replicationDetails.mode === 'primary') {
return true;
}
return false;
}),
message: computed('model.anyReplicationEnabled', 'formattedReplicationMode', function () {
}
get message() {
let msg;
if (this.model.anyReplicationEnabled) {
if (this.args.model.anyReplicationEnabled) {
msg = `This ${this.formattedReplicationMode} secondary has not been enabled. You can do so from the ${this.formattedReplicationMode} Primary.`;
} else {
msg = `This cluster has not been enabled as a ${this.formattedReplicationMode} Secondary. You can do so by enabling replication and adding a secondary from the ${this.formattedReplicationMode} Primary.`;
}
return msg;
}),
});
}
}

View File

@@ -10,6 +10,7 @@ import Component from '@ember/component';
import decodeConfigFromJWT from 'replication/utils/decode-config-from-jwt';
import ReplicationActions from 'core/mixins/replication-actions';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
const DEFAULTS = {
token: null,
@@ -66,13 +67,15 @@ export default Component.extend(ReplicationActions, DEFAULTS, {
this.setProperties(DEFAULTS);
},
submit: task(function* () {
submit: task(
waitFor(function* () {
try {
yield this.submitHandler.perform(...arguments);
} catch (e) {
// do not handle error
}
}),
})
),
actions: {
onSubmit(/*action, mode, data, event*/) {
this.submit.perform(...arguments);

View File

@@ -7,17 +7,20 @@ import { alias } from '@ember/object/computed';
import { service } from '@ember/service';
import Controller from '@ember/controller';
import { task, timeout } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
export default Controller.extend({
router: service(),
rm: service('replication-mode'),
replicationMode: alias('rm.mode'),
waitForNewClusterToInit: task(function* (replicationMode) {
waitForNewClusterToInit: task(
waitFor(function* (replicationMode) {
// waiting for the newly enabled cluster to init
// this ensures we don't hit a capabilities-self error, called in the model of the mode/index route
yield timeout(1000);
this.router.transitionTo('vault.cluster.replication.mode', replicationMode);
}),
})
),
actions: {
onEnable(replicationMode, mode) {
if (replicationMode == 'dr' && mode === 'secondary') {

View File

@@ -17,9 +17,9 @@
</Hds::Breadcrumb>
</p.top>
<p.levelLeft>
<h1 class="has-top-margin-m title is-3" data-test-replication-title={{true}}>
<h1 class="has-top-margin-m title is-3" data-test-replication-title>
{{this.model.replicationModeForDisplay}}
<span class="tag is-light has-text-grey-dark" data-test-replication-mode-display={{true}}>
<span class="tag is-light has-text-grey-dark" data-test-replication-mode-display>
{{this.model.replicationAttrs.modeForHeader}}
</span>
</h1>

View File

@@ -32,7 +32,7 @@
"test:enos": "concurrently --kill-others-on-fail -P -c \"auto\" -n lint:js,lint:hbs,enos \"yarn:lint:js:quiet\" \"yarn:lint:hbs:quiet\" \"node scripts/enos-test-ember.js {@}\" --",
"test:oss": "yarn run test -f='!enterprise' --split=8 --preserve-test-name --parallel",
"test:quick": "node scripts/start-vault.js --split=8 --preserve-test-name --parallel",
"test:quick-oss": "yarn test:quick -f='!enterprise' --split=8 --preserve-test-name --parallel",
"test:quick-oss": "node scripts/start-vault.js -f='!enterprise' --split=8 --preserve-test-name --parallel",
"test:filter": "node scripts/start-vault.js --server -f='!enterprise'",
"types:declare": "declare () { yarn tsc $1 --declaration --allowJs --emitDeclarationOnly --experimentalDecorators --outDir $2; }; declare",
"vault": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8200 vault server -log-level=error -dev -dev-root-token-id=root -dev-ha -dev-transactional",

View File

@@ -59,7 +59,9 @@ module('Acceptance | Enterprise | replication modes', function (hooks) {
await click(s.navLink('Performance'));
assert.strictEqual(currentURL(), '/vault/replication/performance', 'it navigates to the correct page');
await settled();
assert.dom(s.title).hasText('Enable Performance Replication', 'it shows the enable view for performance');
assert
.dom(s.title)
.hasText('Enable Performance Replication', 'it shows the enable view for performance (flaky)');
await click(s.navLink('Disaster Recovery'));
assert.dom(s.title).hasText('Enable Disaster Recovery Replication', 'it shows the enable view for dr');

View File

@@ -90,8 +90,8 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await click('#deny');
await clickTrigger();
const mountPath = searchSelect.options.objectAt(0).text;
await searchSelect.options.objectAt(0).click();
const mountPath = find('[data-test-selected-option="0"]').textContent.trim();
await click('[data-test-secondary-add]');
await pollCluster(this.owner);
@@ -109,7 +109,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
assert.dom('[data-test-mount-config-mode]').includesText(mode, 'show page renders the correct mode');
assert
.dom('[data-test-mount-config-paths]')
.includesText(mountPath, 'show page renders the correct mount path');
.includesText(`${mountPath}/`, 'show page renders the correct mount path');
// delete config by choosing "no filter" in the edit screen
await click('[data-test-replication-link="edit-mount-config"]');
@@ -321,6 +321,7 @@ module('Acceptance | Enterprise | replication', function (hooks) {
await settled();
// navigate using breadcrumbs back to replication.index
assert.dom('[data-test-replication-breadcrumb]').exists('shows the replication breadcrumb (flaky)');
await click('[data-test-replication-breadcrumb] a');
assert

View File

@@ -5,11 +5,14 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { render, settled, waitFor } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { setupMirage } from 'ember-cli-mirage/test-support';
const MODEL = {
replicationMode: 'dr',
dr: { mode: 'primary' },
performance: { mode: 'primary' },
replicationAttrs: {
mode: 'secondary',
clusterId: '12ab',
@@ -19,6 +22,7 @@ const MODEL = {
module('Integration | Component | replication-page', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.beforeEach(function () {
this.set('model', MODEL);
@@ -40,4 +44,29 @@ module('Integration | Component | replication-page', function (hooks) {
await render(hbs`<ReplicationPage @model={{this.model}} />`);
assert.dom('[data-test-layout-loading]').exists();
});
test.skip('it re-fetches data when replication mode changes', async function (assert) {
assert.expect(4);
this.server.get('sys/replication/:mode/status', (schema, req) => {
assert.strictEqual(
req.params.mode,
this.model.replicationMode,
`fetchStatus called with correct mode: ${this.model.replicationMode}`
);
return {
data: {
mode: 'primary',
},
};
});
await render(
hbs`<ReplicationPage @model={{this.model}} as |Page|><Page.header @showTabs={{true}} /></ReplicationPage>`
);
await waitFor('[data-test-replication-title]');
// Title has spaces and newlines, so we can't use hasText because it won't match exactly
assert.dom('[data-test-replication-title]').includesText('Disaster Recovery');
this.set('model', { ...MODEL, replicationMode: 'performance' });
await settled();
assert.dom('[data-test-replication-title]').includesText('Performance');
});
});