Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Fix enabling replication capabilities bug #28371

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/28371.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Fix UI improperly checking capabilities for enabling performance and dr replication
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wording feels strange? Open to alternatives!

```
1 change: 1 addition & 0 deletions ui/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class App extends Application {
dependencies: {
services: [
'auth',
'capabilities',
'flash-messages',
'namespace',
'replication-mode',
Expand Down
17 changes: 8 additions & 9 deletions ui/lib/replication/addon/components/enable-replication-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,15 @@ import { waitFor } from '@ember/test-waiters';
* but otherwise it handles the rest of the form inputs. On success it will clear the form and call the onSuccess callback.
*
* @example
* ```js
* <EnableReplicationForm @replicationMode="dr" @canEnablePrimary={{true}} @canEnableSecondary={{false}} @performanceReplicationDisabled={{false}} @onSuccess={{this.reloadCluster}} />
* @param {string} replicationMode - should be one of "dr" or "performance"
* @param {boolean} canEnablePrimary - if the capabilities allow the user to enable a primary cluster
* @param {boolean} canEnableSecondary - if the capabilities allow the user to enable a secondary cluster
* @param {boolean} performanceMode - should be "primary", "secondary", or "disabled". If enabled, form will show a warning when attempting to enable DR secondary
* @param {Promise} onSuccess - (optional) callback called after successful replication enablement. Must be a promise.
* @param {boolean} doTransition - (optional) if provided, passed to onSuccess callback to determine if a transition should be done
* />
* ```
*
* @param {string} replicationMode - should be one of "dr" or "performance"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔤 alphabetized 🤓

* @param {boolean} canEnablePrimary - if the capabilities allow the user to enable a primary cluster, parent getter returns capabilities based on type (i.e. "dr" or "performance")
* @param {boolean} canEnableSecondary - if the capabilities allow the user to enable a secondary cluster, parent getter returns capabilities based on type (i.e. "dr" or "performance")
* @param {boolean} performanceMode - should be "primary", "secondary", or "disabled". If enabled, form will show a warning when attempting to enable DR secondary
* @param {Promise} onSuccess - (optional) callback called after successful replication enablement. Must be a promise.
* @param {boolean} doTransition - (optional) if provided, passed to onSuccess callback to determine if a transition should be done
*
*/
export default class EnableReplicationFormComponent extends Component {
@service version;
Expand Down
4 changes: 2 additions & 2 deletions ui/lib/replication/addon/components/page/mode-index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
</div>
<EnableReplicationForm
@replicationMode={{@replicationMode}}
@canEnablePrimary={{@cluster.canEnablePrimary}}
@canEnableSecondary={{@cluster.canEnableSecondary}}
@canEnablePrimary={{this.canEnable "Primary"}}
@canEnableSecondary={{this.canEnable "Secondary"}}
@performanceReplicationDisabled={{@cluster.performance.replicationDisabled}}
@performanceMode={{if @cluster.performance.replicationDisabled "disabled" @cluster.performance.modeForUrl}}
@onSuccess={{@onEnableSuccess}}
Expand Down
40 changes: 40 additions & 0 deletions ui/lib/replication/addon/components/page/mode-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/

import Component from '@glimmer/component';

/**
* @module PageModeIndex
*
* @example
* <Page::ModeIndex
* @cluster={{this.model}}
* @onEnableSuccess={{this.onEnableSuccess}}
* @replicationDisabled={{this.replicationForMode.replicationDisabled}
* @replicationMode={{this.replicationMode}}
* />
*
* @param {model} cluster - cluster route model
* @param {function} onEnableSuccess - callback after enabling is successful, handles transition if enabled from the top-level index route
* @param {boolean} replicationDisabled - whether or not replication is enabled
* @param {string} replicationMode - should be "dr" or "performance"
*/
export default class PageModeIndex extends Component {
canEnable = (type) => {
const { cluster, replicationMode } = this.args;
let perm;
if (replicationMode === 'dr') {
// returns canEnablePrimaryDr or canEnableSecondaryDr
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added these comments so future devs see they are used here if performing a global search for the capabilities defined in the route

canEnablePrimaryDr,
canEnableSecondaryDr,
canEnablePrimaryPerformance,
canEnableSecondaryPerformance,

perm = `canEnable${type}Dr`;
}
if (replicationMode === 'performance') {
// returns canEnablePrimaryPerformance or canEnableSecondaryPerformance
perm = `canEnable${type}Performance`;
}
// if there's a problem checking capabilities, default to true
// since the backend can gate as a fallback
return cluster[perm] ?? true;
};
}
20 changes: 20 additions & 0 deletions ui/lib/replication/addon/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,24 @@ import { tracked } from '@glimmer/tracking';

export default class ReplicationIndexController extends ReplicationModeBaseController {
@tracked modeSelection = 'dr';

getPerm(type) {
if (this.modeSelection === 'dr') {
// returns canEnablePrimaryDr or canEnableSecondaryDr
return `canEnable${type}Dr`;
}
if (this.modeSelection === 'performance') {
Comment on lines +13 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to explicitly check for each mode instead of do a ternary statement since that'd mean if there wasn't a mode we'd fallback on a whichever was the falsy capability which didn't feel like the most stable thing to do.

// returns canEnablePrimaryPerformance or canEnableSecondaryPerformance
return `canEnable${type}Performance`;
}
}

// if there's a problem checking capabilities, default to true
// since the backend will gate as a fallback
get canEnablePrimary() {
return this.model[this.getPerm('Primary')] ?? true;
}
get canEnableSecondary() {
return this.model[this.getPerm('Secondary')] ?? true;
}
}
1 change: 1 addition & 0 deletions ui/lib/replication/addon/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const Eng = Engine.extend({
dependencies: {
services: [
'auth',
'capabilities',
'flash-messages',
'namespace',
'replication-mode',
Expand Down
46 changes: 31 additions & 15 deletions ui/lib/replication/addon/routes/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import { service } from '@ember/service';
import { setProperties } from '@ember/object';
import { hash } from 'rsvp';
import Route from '@ember/routing/route';
import ClusterRoute from 'vault/mixins/cluster-route';

Expand All @@ -14,6 +13,23 @@ export default Route.extend(ClusterRoute, {
store: service(),
auth: service(),
router: service(),
capabilities: service(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about not using the new service here so that it more cleanly backported to older versions (this only exists in 1.18 so far), BUT the service is nice because we only make one request for all of these capabilities instead of a request for each path which I think is a win! 🙌


async fetchCapabilities() {
const enablePath = (type, cluster) => `sys/replication/${type}/${cluster}/enable`;
const perms = await this.capabilities.fetchMultiplePaths([
enablePath('dr', 'primary'),
enablePath('dr', 'primary'),
enablePath('performance', 'secondary'),
enablePath('performance', 'secondary'),
]);
return {
canEnablePrimaryDr: perms[enablePath('dr', 'primary')].canUpdate,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

originally I wrote all these variables as canEnableDrPrimary or canEnablePerformancePrimary but I found it easier to read with the type at the end and the cluster in the middle. Dr seemed to get lost in the middle there

canEnableSecondaryDr: perms[enablePath('dr', 'primary')].canUpdate,
canEnablePrimaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
canEnableSecondaryPerformance: perms[enablePath('performance', 'secondary')].canUpdate,
};
},

beforeModel() {
if (this.auth.activeCluster.replicationRedacted) {
Expand All @@ -29,21 +45,21 @@ export default Route.extend(ClusterRoute, {
return this.auth.activeCluster;
},

afterModel(model) {
return hash({
canEnablePrimary: this.store
.findRecord('capabilities', 'sys/replication/primary/enable')
.then((c) => c.canUpdate),
canEnableSecondary: this.store
.findRecord('capabilities', 'sys/replication/secondary/enable')
.then((c) => c.canUpdate),
}).then(({ canEnablePrimary, canEnableSecondary }) => {
setProperties(model, {
canEnablePrimary,
canEnableSecondary,
});
return model;
async afterModel(model) {
const {
canEnablePrimaryDr,
canEnableSecondaryDr,
canEnablePrimaryPerformance,
canEnableSecondaryPerformance,
} = await this.fetchCapabilities();

setProperties(model, {
canEnablePrimaryDr,
canEnableSecondaryDr,
canEnablePrimaryPerformance,
canEnableSecondaryPerformance,
});
return model;
},
actions: {
refresh() {
Expand Down
4 changes: 2 additions & 2 deletions ui/lib/replication/addon/templates/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@
</div>
<EnableReplicationForm
@replicationMode={{this.modeSelection}}
@canEnablePrimary={{this.model.canEnablePrimary}}
@canEnableSecondary={{this.model.canEnableSecondary}}
@canEnablePrimary={{this.canEnablePrimary}}
@canEnableSecondary={{this.canEnableSecondary}}
@performanceReplicationDisabled={{this.model.performance.replicationDisabled}}
@performanceMode={{if this.model.performance.replicationDisabled "disabled" this.model.performance.modeForUrl}}
@onSuccess={{this.onEnableSuccess}}
Expand Down
42 changes: 42 additions & 0 deletions ui/tests/integration/components/page/mode-index-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ const S = {
title: 'h1',
subtitle: 'h2',
enableForm: '[data-test-replication-enable-form]',
enableBtn: '[data-test-replication-enable]',
summary: '[data-test-replication-summary]',
notAllowed: '[data-test-not-allowed]',
};
module('Integration | Component | replication page/mode-index', function (hooks) {
setupRenderingTest(hooks);
Expand Down Expand Up @@ -43,6 +45,8 @@ module('Integration | Component | replication page/mode-index', function (hooks)

assert.dom(S.title).hasText('Enable Disaster Recovery Replication');
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists('Enable button shows by default if no permissions available');
});
test('it renders correctly when replication enabled', async function (assert) {
this.replicationDisabled = false;
Expand All @@ -51,6 +55,24 @@ module('Integration | Component | replication page/mode-index', function (hooks)
assert.dom(S.enableForm).doesNotExist();
assert.dom(S.summary).exists();
});

test('it hides enable button if no permissions', async function (assert) {
this.clusterModel.canEnablePrimaryDr = false;
await this.renderComponent();

assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).exists();
assert.dom(S.enableBtn).doesNotExist();
});

test('it shows enable button if has permissions', async function (assert) {
this.clusterModel.canEnablePrimaryDr = true;
await this.renderComponent();

assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists();
});
});

module('Performance mode', function (hooks) {
Expand All @@ -62,6 +84,8 @@ module('Integration | Component | replication page/mode-index', function (hooks)

assert.dom(S.title).hasText('Enable Performance Replication');
assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists('Enable button shows by default if no permissions available');
});
test('it renders correctly when replication enabled', async function (assert) {
this.replicationDisabled = false;
Expand All @@ -70,5 +94,23 @@ module('Integration | Component | replication page/mode-index', function (hooks)
assert.dom(S.enableForm).doesNotExist();
assert.dom(S.summary).exists();
});

test('it hides enable button if no permissions', async function (assert) {
this.clusterModel.canEnablePrimaryPerformance = false;
await this.renderComponent();

assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).exists();
assert.dom(S.enableBtn).doesNotExist();
});

test('it shows enable button if has permissions', async function (assert) {
this.clusterModel.canEnablePrimaryPerformance = true;
await this.renderComponent();

assert.dom(S.enableForm).exists();
assert.dom(S.notAllowed).doesNotExist();
assert.dom(S.enableBtn).exists();
});
});
});
Loading