diff --git a/changelog/20125.txt b/changelog/20125.txt new file mode 100644 index 000000000..07dd8201d --- /dev/null +++ b/changelog/20125.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: updates clients configuration edit form state based on census reporting configuration +``` \ No newline at end of file diff --git a/ui/app/components/clients/config.js b/ui/app/components/clients/config.js index 74da5ad7b..97faef403 100644 --- a/ui/app/components/clients/config.js +++ b/ui/app/components/clients/config.js @@ -23,9 +23,11 @@ import { task } from 'ember-concurrency'; export default class ConfigComponent extends Component { @service router; + @tracked mode = 'show'; @tracked modalOpen = false; - error = null; + @tracked validations; + @tracked error = null; get infoRows() { return [ @@ -43,38 +45,36 @@ export default class ConfigComponent extends Component { } get modalTitle() { - let content = 'Turn usage tracking off?'; - if (this.args.model && this.args.model.enabled === 'On') { - content = 'Turn usage tracking on?'; - } - return content; + return `Turn usage tracking ${this.args.model.enabled.toLowerCase()}?`; } @(task(function* () { try { yield this.args.model.save(); + this.router.transitionTo('vault.cluster.clients.config'); } catch (err) { this.error = err.message; - return; + this.modalOpen = false; } - this.router.transitionTo('vault.cluster.clients.config'); }).drop()) save; @action - updateBooleanValue(attr, value) { - const valueToSet = value === true ? attr.options.trueValue : attr.options.falseValue; - this.args.model[attr.name] = valueToSet; + toggleEnabled(event) { + this.args.model.enabled = event.target.checked ? 'On' : 'Off'; } @action onSaveChanges(evt) { evt.preventDefault(); + const { isValid, state } = this.args.model.validate(); const changed = this.args.model.changedAttributes(); - if (!changed.enabled) { + if (!isValid) { + this.validations = state; + } else if (changed.enabled) { + this.modalOpen = true; + } else { this.save.perform(); - return; } - this.modalOpen = true; } } diff --git a/ui/app/decorators/model-validations.js b/ui/app/decorators/model-validations.js index 5f598a571..9a69ef77b 100644 --- a/ui/app/decorators/model-validations.js +++ b/ui/app/decorators/model-validations.js @@ -19,6 +19,7 @@ import { get } from '@ember/object'; * options - an optional object for given validator -- min, max, nullable etc. -- see validators in util * * message - string added to the errors array and returned from the validate method if validation fails + * function may also be provided with model as single argument that returns a string * * level - optional string that defaults to 'error'. Currently the only other accepted value is 'warn' * @@ -120,12 +121,14 @@ export function withModelValidations(validations) { : validator(get(this, key), options); // dot notation may be used to define key for nested property if (!passedValidation) { + // message can also be a function + const validationMessage = typeof message === 'function' ? message(this) : message; // consider setting a prop like validationErrors directly on the model // for now return an errors object if (level === 'warn') { - state[key].warnings.push(message); + state[key].warnings.push(validationMessage); } else { - state[key].errors.push(message); + state[key].errors.push(validationMessage); if (isValid) { isValid = false; } diff --git a/ui/app/models/clients/config.js b/ui/app/models/clients/config.js index 0462f6042..0be017a4c 100644 --- a/ui/app/models/clients/config.js +++ b/ui/app/models/clients/config.js @@ -4,32 +4,45 @@ */ import Model, { attr } from '@ember-data/model'; -import { computed } from '@ember/object'; -import attachCapabilities from 'vault/lib/attach-capabilities'; -import { expandAttributeMeta } from 'vault/utils/field-to-attrs'; -import { apiPath } from 'vault/macros/lazy-capabilities'; +import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; +import { withFormFields } from 'vault/decorators/model-form-fields'; +import { withModelValidations } from 'vault/decorators/model-validations'; -const M = Model.extend({ - queriesAvailable: attr('boolean'), // true only if historical data exists, will be false if there is only current month data - retentionMonths: attr('number', { +const validations = { + retentionMonths: [ + { + validator: (model) => parseInt(model.retentionMonths) >= model.minimumRetentionMonths, + message: (model) => + `Retention period must be greater than or equal to ${model.minimumRetentionMonths}.`, + }, + ], +}; + +@withModelValidations(validations) +@withFormFields(['enabled', 'retentionMonths']) +export default class ClientsConfigModel extends Model { + @attr('boolean') queriesAvailable; // true only if historical data exists, will be false if there is only current month data + + @attr('number', { label: 'Retention period', subText: 'The number of months of activity logs to maintain for client tracking.', - }), - enabled: attr('string', { - editType: 'boolean', - trueValue: 'On', - falseValue: 'Off', - label: 'Enable usage data collection', - helpText: - 'Enable or disable client tracking. Keep in mind that disabling tracking will delete the data for the current month.', - }), + }) + retentionMonths; - configAttrs: computed(function () { - const keys = ['enabled', 'retentionMonths']; - return expandAttributeMeta(this, keys); - }), -}); + @attr('number') minimumRetentionMonths; -export default attachCapabilities(M, { - configPath: apiPath`sys/internal/counters/config`, -}); + @attr('string') enabled; + + @attr('boolean') reportingEnabled; + + @attr('date') billingStartTimestamp; + + @lazyCapabilities(apiPath`sys/internal/counters/config`) configPath; + + get canRead() { + return this.configPath.get('canRead') !== false; + } + get canEdit() { + return this.configPath.get('canUpdate') !== false; + } +} diff --git a/ui/app/templates/components/clients/config.hbs b/ui/app/templates/components/clients/config.hbs index 5bee8ae24..f87f76591 100644 --- a/ui/app/templates/components/clients/config.hbs +++ b/ui/app/templates/components/clients/config.hbs @@ -1,80 +1,38 @@ {{#if (eq @mode "edit")}} -
+
- {{#each @model.configAttrs as |attr|}} - {{#if (and (eq attr.type "string") (eq attr.options.editType "boolean"))}} + {{#each @model.formFields as |attr|}} + {{#if (eq attr.name "enabled")}} - {{#if attr.options.helpText}} -

- {{attr.options.helpText}} - {{#if attr.options.docLink}} - - See our documentation - - for help. - {{/if}} -

- {{/if}} +

+ Enable or disable client tracking. Keep in mind that disabling tracking will delete the data for the current + month. +

-
- {{else if (eq attr.type "number")}} -
- - {{#if attr.options.subText}} -

- {{attr.options.subText}} - {{#if attr.options.docLink}} - - See our documentation - - for help. - {{/if}} -

- {{/if}} -
- -
-
+ {{else}} + {{/if}} {{/each}}
- @@ -83,6 +41,7 @@
+ {{else}} -
+
{{#each this.infoRows as |item|}} {{/each}} diff --git a/ui/app/templates/components/clients/dashboard.hbs b/ui/app/templates/components/clients/dashboard.hbs index ebf2fc669..01642cef2 100644 --- a/ui/app/templates/components/clients/dashboard.hbs +++ b/ui/app/templates/components/clients/dashboard.hbs @@ -32,7 +32,7 @@ @title="Data tracking is disabled" @message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration." > - {{#if @model.config.configPath.canUpdate}} + {{#if @model.config.canEdit}}

Go to configuration diff --git a/ui/app/templates/vault/cluster/clients.hbs b/ui/app/templates/vault/cluster/clients.hbs index 25693f6a7..79fab836d 100644 --- a/ui/app/templates/vault/cluster/clients.hbs +++ b/ui/app/templates/vault/cluster/clients.hbs @@ -12,8 +12,11 @@ Dashboard - {{#if (or @model.config.configPath.canRead @model.configPath.canRead)}} - + {{#if (or @model.config.canRead @model.canRead)}} + Configuration {{/if}} diff --git a/ui/app/templates/vault/cluster/clients/config.hbs b/ui/app/templates/vault/cluster/clients/config.hbs index 57b559f75..bdc3bd007 100644 --- a/ui/app/templates/vault/cluster/clients/config.hbs +++ b/ui/app/templates/vault/cluster/clients/config.hbs @@ -1,6 +1,6 @@ - {{#if @model.configPath.canUpdate}} + {{#if @model.canEdit}} Edit configuration diff --git a/ui/tests/integration/components/clients/config-test.js b/ui/tests/integration/components/clients/config-test.js index 9f69f3804..65e8ea3d4 100644 --- a/ui/tests/integration/components/clients/config-test.js +++ b/ui/tests/integration/components/clients/config-test.js @@ -5,57 +5,42 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render, find, click } from '@ember/test-helpers'; -import { resolve } from 'rsvp'; +import { render, find, click, fillIn } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; module('Integration | Component | client count config', function (hooks) { setupRenderingTest(hooks); - - const createAttr = (name, type, options) => { - return { - name, - type, - options, - }; - }; - - const generateModel = (overrides) => { - return { - enabled: 'On', - retentionMonths: 24, - defaultReportMonths: 12, - configAttrs: [ - createAttr('enabled', 'string', { editType: 'boolean' }), - createAttr('retentionMonths', 'number'), - ], - changedAttributes: () => ({}), - save: () => {}, - ...overrides, - }; - }; + setupMirage(hooks); hooks.beforeEach(function () { this.router = this.owner.lookup('service:router'); - this.router.reopen({ - transitionTo() { - return { - followRedirects() { - return resolve(); - }, - }; - }, - }); - const model = generateModel(); - this.model = model; + this.transitionStub = sinon.stub(this.router, 'transitionTo'); + const store = this.owner.lookup('service:store'); + this.createModel = (enabled = 'enable', reporting_enabled = false, minimum_retention_months = 0) => { + store.pushPayload('clients/config', { + modelName: 'clients/config', + id: 'foo', + data: { + enabled, + reporting_enabled, + minimum_retention_months, + retention_months: 24, + }, + }); + this.model = store.peekRecord('clients/config', 'foo'); + }; }); test('it shows the table with the correct rows by default', async function (assert) { + this.createModel(); + await render(hbs``); - assert.dom('[data-test-pricing-metrics-config-table]').exists('Pricing metrics config table exists'); + assert.dom('[data-test-clients-config-table]').exists('Clients config table exists'); const rows = document.querySelectorAll('.info-table-row'); - assert.strictEqual(rows.length, 2, 'renders 2 infotable rows'); + assert.strictEqual(rows.length, 2, 'renders 2 info table rows'); assert.ok( find('[data-test-row-value="Usage data collection"]').textContent.includes('On'), 'Enabled value matches model' @@ -66,72 +51,122 @@ module('Integration | Component | client count config', function (hooks) { ); }); - test('TODO: it shows the config edit form when mode = edit', async function (assert) { - await render(hbs` -

- - `); + test('it should function in edit mode when reporting is disabled', async function (assert) { + assert.expect(13); - assert.dom('[data-test-pricing-metrics-config-form]').exists('Pricing metrics config form exists'); - const fields = document.querySelectorAll('[data-test-field]'); - assert.strictEqual(fields.length, 2, 'renders 2 fields'); - }); - - test('it shows a modal with correct messaging when disabling', async function (assert) { - // Simulates the model when enabled value has been changed from On to Off - const simModel = generateModel({ - enabled: 'Off', - changedAttributes: () => ({ enabled: ['On', 'Off'] }), + this.server.put('/sys/internal/counters/config', (schema, req) => { + const { enabled, retention_months } = JSON.parse(req.requestBody); + const expected = { enabled: 'enable', retention_months: 5 }; + assert.deepEqual(expected, { enabled, retention_months }, 'Correct data sent in PUT request'); + return {}; }); - this.set('model', simModel); + + this.createModel('disable'); + await render(hbs` `); - await click('[data-test-edit-metrics-config-save]'); - assert.dom('.modal.is-active').exists('Modal appears'); + assert.dom('[data-test-input="enabled"]').isNotChecked('Data collection checkbox is not checked'); + assert + .dom('label[for="enabled"]') + .hasText('Data collection is off', 'Correct label renders when data collection is off'); + assert.dom('[data-test-input="retentionMonths"]').hasValue('24', 'Retention months render'); + + await click('[data-test-input="enabled"]'); + await fillIn('[data-test-input="retentionMonths"]', -3); + await click('[data-test-clients-config-save]'); + assert + .dom('[data-test-inline-error-message]') + .hasText( + 'Retention period must be greater than or equal to 0.', + 'Validation error shows for incorrect retention period' + ); + + await fillIn('[data-test-input="retentionMonths"]', 5); + await click('[data-test-clients-config-save]'); + assert.dom('.modal.is-active').exists('Modal renders'); + assert + .dom('[data-test-modal-title] span') + .hasText('Turn usage tracking on?', 'Correct modal title renders'); + assert.dom('[data-test-clients-config-modal="on"]').exists('Correct modal description block renders'); + + await click('[data-test-clients-config-modal="continue"]'); assert.ok( - find('[data-test-modal-title]').textContent.includes('Turn usage tracking off?'), - 'Modal confirming turn tracking off' + this.transitionStub.calledWith('vault.cluster.clients.config'), + 'Route transitions correctly on save success' ); - await click('[data-test-metrics-config-cancel]'); - assert.dom('.modal.is-active').doesNotExist('Modal goes away'); + + await click('[data-test-input="enabled"]'); + await click('[data-test-clients-config-save]'); + assert.dom('.modal.is-active').exists('Modal renders'); + assert + .dom('[data-test-modal-title] span') + .hasText('Turn usage tracking off?', 'Correct modal title renders'); + assert.dom('[data-test-clients-config-modal="off"]').exists('Correct modal description block renders'); + + await click('[data-test-clients-config-modal="cancel"]'); + assert.dom('.modal.is-active').doesNotExist('Modal is hidden on cancel'); }); - test('it shows a modal with correct messaging when enabling', async function (assert) { - // Simulates the model when enabled value has been changed from On to Off - const simModel = generateModel({ - changedAttributes: () => ({ enabled: ['Off', 'On'] }), + test('it should function in edit mode when reporting is enabled', async function (assert) { + assert.expect(6); + + this.server.put('/sys/internal/counters/config', (schema, req) => { + const { enabled, retention_months } = JSON.parse(req.requestBody); + const expected = { enabled: 'enable', retention_months: 48 }; + assert.deepEqual(expected, { enabled, retention_months }, 'Correct data sent in PUT request'); + return {}; }); - this.set('model', simModel); + + this.createModel('enable', true, 24); + await render(hbs` `); - await click('[data-test-edit-metrics-config-save]'); - assert.dom('.modal.is-active').exists('Modal appears'); - assert.ok( - find('[data-test-modal-title]').textContent.includes('Turn usage tracking on?'), - 'Modal confirming turn tracking on' - ); - await click('[data-test-metrics-config-cancel]'); - assert.dom('.modal.is-active').doesNotExist('Modal goes away'); + assert.dom('[data-test-input="enabled"]').isChecked('Data collection input is checked'); + assert + .dom('[data-test-input="enabled"]') + .isDisabled('Data collection input disabled when reporting is enabled'); + assert + .dom('label[for="enabled"]') + .hasText('Data collection is on', 'Correct label renders when data collection is on'); + assert.dom('[data-test-input="retentionMonths"]').hasValue('24', 'Retention months render'); + + await fillIn('[data-test-input="retentionMonths"]', 5); + await click('[data-test-clients-config-save]'); + assert + .dom('[data-test-inline-error-message]') + .hasText( + 'Retention period must be greater than or equal to 24.', + 'Validation error shows for incorrect retention period' + ); + + await fillIn('[data-test-input="retentionMonths"]', 48); + await click('[data-test-clients-config-save]'); }); - test('it does not show a modal on save if enable left unchanged', async function (assert) { - // Simulates the model when something other than enabled changed - const simModel = generateModel({ - changedAttributes: () => ({ retentionMonths: [24, '48'] }), + test('it should not show modal when data collection is not changed', async function (assert) { + assert.expect(1); + + this.server.put('/sys/internal/counters/config', (schema, req) => { + const { enabled, retention_months } = JSON.parse(req.requestBody); + const expected = { enabled: 'enable', retention_months: 5 }; + assert.deepEqual(expected, { enabled, retention_months }, 'Correct data sent in PUT request'); + return {}; }); - this.set('model', simModel); + + this.createModel(); + await render(hbs` `); - await click('[data-test-edit-metrics-config-save]'); - assert.dom('.modal.is-active').doesNotExist('No modal appears'); + await fillIn('[data-test-input="retentionMonths"]', 5); + await click('[data-test-clients-config-save]'); }); });