From 42e1ba2110b74adb74f45b003f5c73a044b69abb Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Fri, 2 Dec 2022 10:42:14 -0600 Subject: [PATCH] UI: PKI Roles Edit (#18194) --- ui/app/models/pki/role.js | 2 +- ui/app/styles/app.scss | 1 + ui/app/styles/core/helpers.scss | 14 ++ ui/app/styles/engines.scss | 2 + .../styles/pki/pki-not-valid-after-form.scss | 3 + .../core/addon/components/info-table-row.hbs | 2 + .../core/addon/components/info-table-row.js | 9 ++ .../components/radio-select-ttl-or-string.hbs | 49 ------- .../components/radio-select-ttl-or-string.js | 52 ------- ui/lib/core/addon/components/ttl-picker.js | 37 +---- ui/lib/core/addon/decorators/confirm-leave.js | 52 +++++++ ui/lib/core/addon/utils/duration-utils.ts | 41 ++++++ .../components/radio-select-ttl-or-string.js | 1 - ui/lib/core/app/decorators/confirm-leave.js | 1 + .../components/page/pki-role-details.hbs | 13 +- .../components/pki-not-valid-after-form.hbs | 56 ++++++++ .../components/pki-not-valid-after-form.ts | 78 ++++++++++ ui/lib/pki/addon/components/pki-role-form.hbs | 22 +-- ui/lib/pki/addon/components/pki-role-form.js | 13 ++ ui/lib/pki/addon/routes/roles/create.js | 11 +- ui/lib/pki/addon/routes/roles/role/edit.js | 14 +- .../pki/addon/templates/roles/role/edit.hbs | 6 +- .../helpers/pki/pki-not-valid-after-form.js | 10 ++ ui/tests/helpers/pki/roles/page-details.js | 1 + .../components/info-table-row-test.js | 16 ++- .../pki/pki-not-valid-after-form-test.js | 133 ++++++++++++++++++ .../pki/radio-select-ttl-or-string-test.js | 87 ------------ ...-test.js => page-pki-role-details-test.js} | 25 +++- 28 files changed, 498 insertions(+), 253 deletions(-) create mode 100644 ui/app/styles/engines.scss create mode 100644 ui/app/styles/pki/pki-not-valid-after-form.scss delete mode 100644 ui/lib/core/addon/components/radio-select-ttl-or-string.hbs delete mode 100644 ui/lib/core/addon/components/radio-select-ttl-or-string.js create mode 100644 ui/lib/core/addon/decorators/confirm-leave.js create mode 100644 ui/lib/core/addon/utils/duration-utils.ts delete mode 100644 ui/lib/core/app/components/radio-select-ttl-or-string.js create mode 100644 ui/lib/core/app/decorators/confirm-leave.js create mode 100644 ui/lib/pki/addon/components/pki-not-valid-after-form.hbs create mode 100644 ui/lib/pki/addon/components/pki-not-valid-after-form.ts create mode 100644 ui/tests/helpers/pki/pki-not-valid-after-form.js create mode 100644 ui/tests/integration/components/pki/pki-not-valid-after-form-test.js delete mode 100644 ui/tests/integration/components/pki/radio-select-ttl-or-string-test.js rename ui/tests/integration/components/pki/roles/{page-details-test.js => page-pki-role-details-test.js} (69%) diff --git a/ui/app/models/pki/role.js b/ui/app/models/pki/role.js index efa8a7a0fd..1c282d36e6 100644 --- a/ui/app/models/pki/role.js +++ b/ui/app/models/pki/role.js @@ -24,6 +24,7 @@ export default class PkiRoleModel extends Model { @attr('string', { label: 'Role name', fieldValue: 'name', + editDisabled: true, }) name; @@ -50,7 +51,6 @@ export default class PkiRoleModel extends Model { helperTextEnabled: 'Also called the not_before_duration property. Allows certificates to be valid for a certain time period before now. This is useful to correct clock misalignment on various systems when setting up your CA.', editType: 'ttl', - hideToggle: true, defaultValue: '30s', // The API type is "duration" which accepts both an integer and string e.g. 30 || '30s' }) notBeforeDuration; diff --git a/ui/app/styles/app.scss b/ui/app/styles/app.scss index 765038eaa9..f7c9ece6f2 100644 --- a/ui/app/styles/app.scss +++ b/ui/app/styles/app.scss @@ -1,6 +1,7 @@ @import 'ember-basic-dropdown'; @import 'ember-power-select'; @import './core'; +@import './engines'; @mixin font-face($name) { @font-face { diff --git a/ui/app/styles/core/helpers.scss b/ui/app/styles/core/helpers.scss index 0a616427e4..6c9a9be2ac 100644 --- a/ui/app/styles/core/helpers.scss +++ b/ui/app/styles/core/helpers.scss @@ -287,3 +287,17 @@ ul.bullet { .has-text-align-center { text-align: center; } +// Screen Readers only +.sr-only { + border: 0 !important; + clip: rect(1px, 1px, 1px, 1px) !important; + -webkit-clip-path: inset(50%) !important; + clip-path: inset(50%) !important; + height: 1px !important; + overflow: hidden !important; + margin: -1px !important; + padding: 0 !important; + position: absolute !important; + width: 1px !important; + white-space: nowrap !important; +} diff --git a/ui/app/styles/engines.scss b/ui/app/styles/engines.scss new file mode 100644 index 0000000000..7ce7e3121c --- /dev/null +++ b/ui/app/styles/engines.scss @@ -0,0 +1,2 @@ +// PKI Engine styles +@import './pki/pki-not-valid-after-form'; diff --git a/ui/app/styles/pki/pki-not-valid-after-form.scss b/ui/app/styles/pki/pki-not-valid-after-form.scss new file mode 100644 index 0000000000..7a812ded16 --- /dev/null +++ b/ui/app/styles/pki/pki-not-valid-after-form.scss @@ -0,0 +1,3 @@ +.pki-radiogroup-label { + align-items: baseline; +} diff --git a/ui/lib/core/addon/components/info-table-row.hbs b/ui/lib/core/addon/components/info-table-row.hbs index c390dc6063..1ceb489ce6 100644 --- a/ui/lib/core/addon/components/info-table-row.hbs +++ b/ui/lib/core/addon/components/info-table-row.hbs @@ -61,6 +61,8 @@ {{/if}} {{else if @formatDate}} {{date-format @value @formatDate}} + {{else if @formatTtl}} + {{this.formattedTtl}} {{else}} {{#if (eq @type "array")}} - - - -
- - -
\ No newline at end of file diff --git a/ui/lib/core/addon/components/radio-select-ttl-or-string.js b/ui/lib/core/addon/components/radio-select-ttl-or-string.js deleted file mode 100644 index 2686f7301f..0000000000 --- a/ui/lib/core/addon/components/radio-select-ttl-or-string.js +++ /dev/null @@ -1,52 +0,0 @@ -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -import { tracked } from '@glimmer/tracking'; - -/** - * @module RadioSelectTtlOrString - * `RadioSelectTtlOrString` components are yielded out within the formField component when the editType on the model is yield. - * The component is two radio buttons, where the first option is a TTL, and the second option is an input field without a title. - * This component is used in the PKI engine inside various forms. - * - * @example - * ```js - * {{#each @model.fields as |attr|}} - * - * {{/each}} - * ``` - * @param {Model} model - Ember Data model that `attr` is defined on. - * @param {Object} attr - Usually derived from ember model `attributes` lookup, and all members of `attr.options` are optional. - */ - -export default class RadioSelectTtlOrString extends Component { - @tracked groupValue = 'ttl'; - @tracked ttlTime; - @tracked notAfter; - - @action onRadioButtonChange(selection) { - this.groupValue = selection; - // Clear the previous selection if they have clicked the other radio button. - if (selection === 'specificDate') { - this.args.model.set('ttl', ''); - this.ttlTime = ''; - } - if (selection === 'ttl') { - this.args.model.set('notAfter', ''); - this.notAfter = ''; - this.args.model.set('ttl', this.ttlTime); - } - } - - @action setAndBroadcastTtl(value) { - const valueToSet = value.enabled === true ? `${value.seconds}s` : 0; - if (this.groupValue === 'specificDate') { - // do not save ttl on the model until the ttl radio button is selected - return; - } - this.args.model.set('ttl', `${valueToSet}`); - } - - @action setAndBroadcastInput(event) { - this.args.model.set('notAfter', event.target.value); - } -} diff --git a/ui/lib/core/addon/components/ttl-picker.js b/ui/lib/core/addon/components/ttl-picker.js index 8560fdd9aa..41554087db 100644 --- a/ui/lib/core/addon/components/ttl-picker.js +++ b/ui/lib/core/addon/components/ttl-picker.js @@ -28,37 +28,12 @@ import Duration from '@icholy/duration'; import { guidFor } from '@ember/object/internals'; import Ember from 'ember'; import { restartableTask, timeout } from 'ember-concurrency'; - -export const secondsMap = { - s: 1, - m: 60, - h: 3600, - d: 86400, -}; -const convertToSeconds = (time, unit) => { - return time * secondsMap[unit]; -}; -const convertFromSeconds = (seconds, unit) => { - return seconds / secondsMap[unit]; -}; -const goSafeConvertFromSeconds = (seconds, unit) => { - // Go only accepts s, m, or h units - const u = unit === 'd' ? 'h' : unit; - return convertFromSeconds(seconds, u) + u; -}; -const largestUnitFromSeconds = (seconds) => { - let unit = 's'; - if (seconds === 0) return unit; - // get largest unit with no remainder - if (seconds % secondsMap.d === 0) { - unit = 'd'; - } else if (seconds % secondsMap.h === 0) { - unit = 'h'; - } else if (seconds % secondsMap.m === 0) { - unit = 'm'; - } - return unit; -}; +import { + convertFromSeconds, + convertToSeconds, + goSafeConvertFromSeconds, + largestUnitFromSeconds, +} from 'core/utils/duration-utils'; export default class TtlPickerComponent extends Component { @tracked enableTTL = false; @tracked recalculateSeconds = false; diff --git a/ui/lib/core/addon/decorators/confirm-leave.js b/ui/lib/core/addon/decorators/confirm-leave.js new file mode 100644 index 0000000000..7e8e142822 --- /dev/null +++ b/ui/lib/core/addon/decorators/confirm-leave.js @@ -0,0 +1,52 @@ +import { action } from '@ember/object'; +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +/** + * Confirm that the user wants to discard unsaved changes before leaving the page. + * This decorator hooks into the willTransition action. If you override setupController, + * be sure to set 'model' on the controller to store data or this won't work. + */ +export function withConfirmLeave() { + return function decorator(SuperClass) { + if (!Object.prototype.isPrototypeOf.call(Route, SuperClass)) { + // eslint-disable-next-line + console.error( + 'withConfirmLeave decorator must be used on instance of ember Route class. Decorator not applied to returned class' + ); + return SuperClass; + } + return class ConfirmLeave extends SuperClass { + @service store; + + @action + willTransition(transition) { + try { + super.willTransition(...arguments); + } catch (e) { + // if the SuperClass doesn't have willTransition + // defined it will throw an error. + } + const model = this.controller.get('model'); + if (model && model.hasDirtyAttributes) { + if ( + window.confirm( + 'You have unsaved changes. Navigating away will discard these changes. Are you sure you want to discard your changes?' + ) + ) { + // error is thrown when you attempt to unload a record that is inFlight (isSaving) + if (!model || !model.unloadRecord || model.isSaving) { + return; + } + model.rollbackAttributes(); + model.destroy(); + return true; + } else { + transition.abort(); + return false; + } + } + } + }; + }; +} diff --git a/ui/lib/core/addon/utils/duration-utils.ts b/ui/lib/core/addon/utils/duration-utils.ts new file mode 100644 index 0000000000..80c2108a31 --- /dev/null +++ b/ui/lib/core/addon/utils/duration-utils.ts @@ -0,0 +1,41 @@ +/** + * These utils are used for managing Duration type values + * (eg. '30m', '365d'). Most often used in the context of TTLs + */ +interface SecondsMap { + s: 1; + m: 60; + h: 3600; + d: 86400; + [key: string]: number; +} +export const secondsMap: SecondsMap = { + s: 1, + m: 60, + h: 3600, + d: 86400, +}; +export const convertToSeconds = (time: number, unit: string) => { + return time * (secondsMap[unit] || 1); +}; +export const convertFromSeconds = (seconds: number, unit: string) => { + return seconds / (secondsMap[unit] || 1); +}; +export const goSafeConvertFromSeconds = (seconds: number, unit: string) => { + // Go only accepts s, m, or h units + const u = unit === 'd' ? 'h' : unit; + return convertFromSeconds(seconds, u) + u; +}; +export const largestUnitFromSeconds = (seconds: number) => { + let unit = 's'; + if (seconds === 0) return unit; + // get largest unit with no remainder + if (seconds % secondsMap.d === 0) { + unit = 'd'; + } else if (seconds % secondsMap.h === 0) { + unit = 'h'; + } else if (seconds % secondsMap.m === 0) { + unit = 'm'; + } + return unit; +}; diff --git a/ui/lib/core/app/components/radio-select-ttl-or-string.js b/ui/lib/core/app/components/radio-select-ttl-or-string.js deleted file mode 100644 index bc3f167f65..0000000000 --- a/ui/lib/core/app/components/radio-select-ttl-or-string.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'core/components/radio-select-ttl-or-string'; diff --git a/ui/lib/core/app/decorators/confirm-leave.js b/ui/lib/core/app/decorators/confirm-leave.js new file mode 100644 index 0000000000..3208d604b9 --- /dev/null +++ b/ui/lib/core/app/decorators/confirm-leave.js @@ -0,0 +1 @@ +export { withConfirmLeave } from 'core/decorators/confirm-leave'; diff --git a/ui/lib/pki/addon/components/page/pki-role-details.hbs b/ui/lib/pki/addon/components/page/pki-role-details.hbs index 56997374b2..7e458c1482 100644 --- a/ui/lib/pki/addon/components/page/pki-role-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-role-details.hbs @@ -50,7 +50,7 @@ > {{#if (gt val.length 0)}} {{#each val as |key|}} - {{key}}, + {{key}}, {{/each}} {{else}} None @@ -62,14 +62,23 @@ @value={{not val}} @alwaysRender={{true}} /> + {{else if (eq attr.name "customTtl")}} + {{! Show either notAfter or ttl }} + {{else}} {{/if}} {{/let}} diff --git a/ui/lib/pki/addon/components/pki-not-valid-after-form.hbs b/ui/lib/pki/addon/components/pki-not-valid-after-form.hbs new file mode 100644 index 0000000000..0fd6024cf4 --- /dev/null +++ b/ui/lib/pki/addon/components/pki-not-valid-after-form.hbs @@ -0,0 +1,56 @@ +
+ +
+ + {{#if (eq this.groupValue "ttl")}} + + + + {{/if}} +
+
+
+ +
+ + {{#if (eq this.groupValue "specificDate")}} + + + {{/if}} +
+
\ No newline at end of file diff --git a/ui/lib/pki/addon/components/pki-not-valid-after-form.ts b/ui/lib/pki/addon/components/pki-not-valid-after-form.ts new file mode 100644 index 0000000000..d979138ef3 --- /dev/null +++ b/ui/lib/pki/addon/components/pki-not-valid-after-form.ts @@ -0,0 +1,78 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { HTMLElementEvent } from 'forms'; +import { format } from 'date-fns'; + +/** + * components are used to manage two mutually exclusive role options in the form. + */ +interface Args { + model: { + notAfter: string; + ttl: string | number; + set: (key: string, value: string | number) => void; + }; +} + +export default class RadioSelectTtlOrString extends Component { + @tracked groupValue = 'ttl'; + @tracked cachedNotAfter: string; + @tracked cachedTtl: string | number; + @tracked formDate: string; + + constructor(owner: unknown, args: Args) { + super(owner, args); + const { model } = this.args; + this.cachedNotAfter = model.notAfter || ''; + this.formDate = this.calculateFormDate(model.notAfter); + this.cachedTtl = model.ttl || ''; + if (model.notAfter) { + this.groupValue = 'specificDate'; + } + } + + calculateFormDate(value: string) { + // API expects and returns full ISO string + // but the form input only accepts yyyy-MM-dd format + if (value) { + return format(new Date(value), 'yyyy-MM-dd'); + } + return ''; + } + + @action onRadioButtonChange(selection: string) { + this.groupValue = selection; + // Clear the previous selection if they have clicked the other radio button. + if (selection === 'specificDate') { + this.args.model.ttl = ''; + this.args.model.notAfter = this.cachedNotAfter; + this.formDate = this.calculateFormDate(this.cachedNotAfter); + } + if (selection === 'ttl') { + this.args.model.notAfter = ''; + this.args.model.ttl = this.cachedTtl; + this.formDate = ''; + } + } + + @action setAndBroadcastTtl(ttlObject: { enabled: boolean; goSafeTimeString: string }) { + const { enabled, goSafeTimeString } = ttlObject; + if (this.groupValue === 'specificDate') { + // do not save ttl on the model unless the ttl radio button is selected + return; + } + const ttlVal = enabled === true ? goSafeTimeString : 0; + this.cachedTtl = ttlVal; + this.args.model.ttl = ttlVal; + } + + @action setAndBroadcastInput(evt: HTMLElementEvent) { + const setDate = evt.target.valueAsDate?.toISOString(); + if (!setDate) return; + + this.cachedNotAfter = setDate; + this.args.model.notAfter = setDate; + this.formDate = this.calculateFormDate(setDate); + } +} diff --git a/ui/lib/pki/addon/components/pki-role-form.hbs b/ui/lib/pki/addon/components/pki-role-form.hbs index 633ca1a540..4be623d7e3 100644 --- a/ui/lib/pki/addon/components/pki-role-form.hbs +++ b/ui/lib/pki/addon/components/pki-role-form.hbs @@ -1,26 +1,14 @@ - -
  • - - / - - - {{@model.backend}} - -
  • -
    +
    -

    +

    {{#if @model.isNew}} Create a PKI role {{else}} - Edit a - {{@model.id}} + Edit + {{@model.name}} {{/if}}

    @@ -43,7 +31,7 @@ @modelValidations={{this.modelValidations}} @showHelpText={{false}} > - + {{/each}} {{else}} diff --git a/ui/lib/pki/addon/components/pki-role-form.js b/ui/lib/pki/addon/components/pki-role-form.js index 9e138fabe2..423e16d28b 100644 --- a/ui/lib/pki/addon/components/pki-role-form.js +++ b/ui/lib/pki/addon/components/pki-role-form.js @@ -27,6 +27,19 @@ export default class PkiRoleForm extends Component { @tracked invalidFormAlert; @tracked modelValidations; + get breadcrumbs() { + const backend = this.args.model.backend || 'pki'; + const crumbs = [ + { label: 'secrets', route: 'secrets', linkExternal: true }, + { label: backend, route: 'overview' }, + { label: 'roles', route: 'roles.index' }, + ]; + if (!this.args.model.isNew) { + crumbs.push({ label: this.args.model.id, route: 'roles.role.details' }, { label: 'edit' }); + } + return crumbs; + } + @task *save(event) { event.preventDefault(); diff --git a/ui/lib/pki/addon/routes/roles/create.js b/ui/lib/pki/addon/routes/roles/create.js index fc45ba4626..a24a4b53cf 100644 --- a/ui/lib/pki/addon/routes/roles/create.js +++ b/ui/lib/pki/addon/routes/roles/create.js @@ -1,14 +1,11 @@ -import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; +import { withConfirmLeave } from 'core/decorators/confirm-leave'; +import PkiRolesIndexRoute from '.'; -export default class PkiRolesCreateRoute extends Route { +@withConfirmLeave() +export default class PkiRolesCreateRoute extends PkiRolesIndexRoute { @service store; @service secretMountPath; - @service pathHelp; - - beforeModel() { - return this.pathHelp.getNewModel('pki/role', 'pki'); - } model() { return this.store.createRecord('pki/role', { diff --git a/ui/lib/pki/addon/routes/roles/role/edit.js b/ui/lib/pki/addon/routes/roles/role/edit.js index b6a9f21012..04eb231cc7 100644 --- a/ui/lib/pki/addon/routes/roles/role/edit.js +++ b/ui/lib/pki/addon/routes/roles/role/edit.js @@ -1,3 +1,13 @@ -import Route from '@ember/routing/route'; +import { withConfirmLeave } from 'core/decorators/confirm-leave'; +import PkiRolesIndexRoute from '../index'; -export default class PkiRoleEditRoute extends Route {} +@withConfirmLeave() +export default class PkiRoleEditRoute extends PkiRolesIndexRoute { + model() { + const { role } = this.paramsFor('roles/role'); + return this.store.queryRecord('pki/role', { + backend: this.secretMountPath.currentPath, + id: role, + }); + } +} diff --git a/ui/lib/pki/addon/templates/roles/role/edit.hbs b/ui/lib/pki/addon/templates/roles/role/edit.hbs index 716c573293..494e252b78 100644 --- a/ui/lib/pki/addon/templates/roles/role/edit.hbs +++ b/ui/lib/pki/addon/templates/roles/role/edit.hbs @@ -1 +1,5 @@ -roles.role.edit \ No newline at end of file + \ No newline at end of file diff --git a/ui/tests/helpers/pki/pki-not-valid-after-form.js b/ui/tests/helpers/pki/pki-not-valid-after-form.js new file mode 100644 index 0000000000..2e8036e87d --- /dev/null +++ b/ui/tests/helpers/pki/pki-not-valid-after-form.js @@ -0,0 +1,10 @@ +export const SELECTORS = { + radioTtl: '[data-test-radio-button="ttl"]', + radioTtlLabel: '[data-test-radio-label="ttl"]', + radioDate: '[data-test-radio-button="not_after"]', + radioDateLabel: '[data-test-radio-label="specificDate"]', + ttlForm: '[data-test-ttl-inputs]', + ttlTimeInput: '[data-test-ttl-value="TTL"]', + ttlUnitInput: '[data-test-select="ttl-unit"]', + dateInput: '[data-test-input="not_after"]', +}; diff --git a/ui/tests/helpers/pki/roles/page-details.js b/ui/tests/helpers/pki/roles/page-details.js index b1e3851c12..c7529e0d76 100644 --- a/ui/tests/helpers/pki/roles/page-details.js +++ b/ui/tests/helpers/pki/roles/page-details.js @@ -6,4 +6,5 @@ export const SELECTORS = { noStoreValue: '[data-test-value-div="Store in storage backend"]', keyUsageValue: '[data-test-value-div="Key usage"]', extKeyUsageValue: '[data-test-value-div="Ext key usage"]', + customTtlValue: '[data-test-value-div="Issued certificates expire after"]', }; diff --git a/ui/tests/integration/components/info-table-row-test.js b/ui/tests/integration/components/info-table-row-test.js index 281a1f77e6..598a2a508c 100644 --- a/ui/tests/integration/components/info-table-row-test.js +++ b/ui/tests/integration/components/info-table-row-test.js @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import { resolve } from 'rsvp'; import Service from '@ember/service'; import { setupRenderingTest } from 'ember-qunit'; -import { render, triggerEvent } from '@ember/test-helpers'; +import { render, settled, triggerEvent } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; const VALUE = 'test value'; @@ -264,4 +264,18 @@ module('Integration | Component | InfoTableRow', function (hooks) { assert.dom('[data-test-value-div]').hasText(yearString, 'Renders date with passed format'); }); + + test('Formats the value as TTL when formatTtl present', async function (assert) { + this.set('value', 6000); + await render(hbs``); + + assert.dom('[data-test-value-div]').hasText('100m', 'Translates number value to largest unit'); + this.set('value', '45m'); + await settled(); + assert.dom('[data-test-value-div]').hasText('45m', 'Renders non-number values as-is'); + }); }); diff --git a/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js b/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js new file mode 100644 index 0000000000..d6c8f3eb63 --- /dev/null +++ b/ui/tests/integration/components/pki/pki-not-valid-after-form-test.js @@ -0,0 +1,133 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, click, fillIn } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupEngine } from 'ember-engines/test-support'; +import { SELECTORS } from 'vault/tests/helpers/pki/pki-not-valid-after-form'; + +module('Integration | Component | pki-not-valid-after-form', function (hooks) { + setupRenderingTest(hooks); + setupEngine(hooks, 'pki'); + + hooks.beforeEach(function () { + this.store = this.owner.lookup('service:store'); + this.model = this.store.createRecord('pki/role', { backend: 'pki' }); + this.attr = { + helpText: '', + options: { + helperTextEnabled: 'toggled on and shows text', + }, + }; + }); + + test('it should render the component with ttl selected by default', async function (assert) { + assert.expect(3); + await render( + hbs` +
    + +
    + `, + { owner: this.engine } + ); + assert.dom(SELECTORS.ttlForm).exists('shows the TTL picker'); + assert.dom(SELECTORS.ttlTimeInput).hasValue('', 'default TTL is empty'); + assert.dom(SELECTORS.radioTtl).isChecked('ttl is selected by default'); + }); + + test('it clears and resets model properties from cache when changing radio selection', async function (assert) { + await render( + hbs` +
    + +
    + `, + { owner: this.engine } + ); + assert.dom(SELECTORS.radioTtl).isChecked('notBeforeDate radio is selected'); + assert.dom(SELECTORS.ttlForm).exists({ count: 1 }, 'shows TTL form'); + assert.dom(SELECTORS.radioDate).isNotChecked('NotAfter selection not checked'); + assert.dom(SELECTORS.dateInput).doesNotExist('does not show date input field'); + + await click(SELECTORS.radioDateLabel); + + assert.dom(SELECTORS.radioDate).isChecked('selects NotAfter radio when label clicked'); + assert.dom(SELECTORS.dateInput).exists({ count: 1 }, 'shows date input field'); + assert.dom(SELECTORS.radioTtl).isNotChecked('notBeforeDate radio is deselected'); + assert.dom(SELECTORS.ttlForm).doesNotExist('hides TTL form'); + + const utcDate = '1994-11-05'; + const notAfterExpected = '1994-11-05T00:00:00.000Z'; + const ttlDate = 1; + await fillIn('[data-test-input="not_after"]', utcDate); + assert.strictEqual( + this.model.notAfter, + notAfterExpected, + 'sets the model property notAfter when this value is selected and filled in.' + ); + await click('[data-test-radio-button="ttl"]'); + assert.strictEqual( + this.model.notAfter, + '', + 'The notAfter is cleared on the model because the radio button was selected.' + ); + await fillIn('[data-test-ttl-value="TTL"]', ttlDate); + assert.strictEqual( + this.model.ttl, + '1s', + 'The ttl is now saved on the model because the radio button was selected.' + ); + + await click('[data-test-radio-button="not_after"]'); + assert.strictEqual(this.model.ttl, '', 'TTL is cleared after radio select.'); + assert.strictEqual(this.model.notAfter, notAfterExpected, 'notAfter gets populated from local cache'); + }); + test('Form renders properly for edit when TTL present', async function (assert) { + this.model = this.store.createRecord('pki/role', { backend: 'pki', ttl: 6000 }); + await render( + hbs` +
    + +
    + `, + { owner: this.engine } + ); + assert.dom(SELECTORS.radioTtl).isChecked('notBeforeDate radio is selected'); + assert.dom(SELECTORS.ttlForm).exists({ count: 1 }, 'shows TTL form'); + assert.dom(SELECTORS.radioDate).isNotChecked('NotAfter selection not checked'); + assert.dom(SELECTORS.dateInput).doesNotExist('does not show date input field'); + + assert.dom(SELECTORS.ttlTimeInput).hasValue('100', 'TTL value is correctly shown'); + assert.dom(SELECTORS.ttlUnitInput).hasValue('m', 'TTL unit is correctly shown'); + }); + test('Form renders properly for edit when notAfter present', async function (assert) { + const utcDate = '1994-11-05T00:00:00.000Z'; + this.model = this.store.createRecord('pki/role', { backend: 'pki', notAfter: utcDate }); + await render( + hbs` +
    + +
    + `, + { owner: this.engine } + ); + assert.dom(SELECTORS.radioDate).isChecked('notAfter radio is selected'); + assert.dom(SELECTORS.dateInput).exists({ count: 1 }, 'shows date picker'); + assert.dom(SELECTORS.radioTtl).isNotChecked('ttl radio not selected'); + assert.dom(SELECTORS.ttlForm).doesNotExist('does not show date TTL picker'); + // Due to timezones, can't check specific match on input date + assert.dom(SELECTORS.dateInput).hasAnyValue('date input shows date'); + }); +}); diff --git a/ui/tests/integration/components/pki/radio-select-ttl-or-string-test.js b/ui/tests/integration/components/pki/radio-select-ttl-or-string-test.js deleted file mode 100644 index a0e5a9bf70..0000000000 --- a/ui/tests/integration/components/pki/radio-select-ttl-or-string-test.js +++ /dev/null @@ -1,87 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click, fillIn } from '@ember/test-helpers'; -import { hbs } from 'ember-cli-htmlbars'; -import { setupEngine } from 'ember-engines/test-support'; - -module('Integration | Component | radio-select-ttl-or-string', function (hooks) { - setupRenderingTest(hooks); - setupEngine(hooks, 'pki'); - - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.model = this.store.createRecord('pki/role'); - this.model.backend = 'pki'; - this.attr = { - helpText: '', - options: { - helperTextEnabled: 'toggled on and shows text', - }, - }; - }); - - test('it should render the component and init with ttl selected', async function (assert) { - assert.expect(3); - await render( - hbs` -
    - -
    - `, - { owner: this.engine } - ); - assert.dom('[data-test-ttl-inputs]').exists('shows the TTL component'); - assert.dom('[data-test-ttl-value]').hasValue('', 'default TTL is empty'); - assert.dom('[data-test-radio-button="ttl"]').isChecked('ttl is selected by default'); - }); - - test('it should set the model properties ttl or notAfter based on the radio button selections', async function (assert) { - assert.expect(7); - await render( - hbs` -
    - -
    - `, - { owner: this.engine } - ); - assert.dom('[data-test-input="not_after"]').doesNotExist('does not show input field on initial render'); - - await click('[data-test-radio-button="not_after"]'); - assert - .dom('[data-test-input="not_after"]') - .exists('does show input field after clicking the radio button'); - - const utcDate = '1994-11-05T08:15:30-05:0'; - const ttlDate = 1; - await fillIn('[data-test-input="not_after"]', utcDate); - assert.strictEqual( - this.model.notAfter, - utcDate, - 'sets the model property notAfter when this value is selected and filled in.' - ); - - await click('[data-test-radio-button="ttl"]'); - assert.strictEqual( - this.model.notAfter, - '', - 'The notAfter is cleared on the model because the radio button was selected.' - ); - await fillIn('[data-test-ttl-value="TTL"]', ttlDate); - assert.strictEqual( - this.model.ttl, - '1s', - 'The ttl is now saved on the model because the radio button was selected.' - ); - - await click('[data-test-radio-button="not_after"]'); - assert.strictEqual(this.model.ttl, '', 'TTL is cleared after radio select.'); - assert.strictEqual(this.model.notAfter, '', 'notAfter is cleared after radio select.'); - }); -}); diff --git a/ui/tests/integration/components/pki/roles/page-details-test.js b/ui/tests/integration/components/pki/roles/page-pki-role-details-test.js similarity index 69% rename from ui/tests/integration/components/pki/roles/page-details-test.js rename to ui/tests/integration/components/pki/roles/page-pki-role-details-test.js index f77a3e6173..ba48c285e6 100644 --- a/ui/tests/integration/components/pki/roles/page-details-test.js +++ b/ui/tests/integration/components/pki/roles/page-pki-role-details-test.js @@ -13,15 +13,16 @@ module('Integration | Component | pki role details page', function (hooks) { this.store = this.owner.lookup('service:store'); this.model = this.store.createRecord('pki/role', { name: 'Foobar', + backend: 'pki', noStore: false, keyUsage: [], extKeyUsage: ['bar', 'baz'], + ttl: 600, }); - this.model.backend = 'pki'; }); test('it should render the page component', async function (assert) { - assert.expect(7); + assert.expect(8); await render( hbs` @@ -38,5 +39,25 @@ module('Integration | Component | pki role details page', function (hooks) { .dom(SELECTORS.extKeyUsageValue) .hasText('bar, baz,', 'Key usage shows comma-joined values when array has items'); assert.dom(SELECTORS.noStoreValue).containsText('Yes', 'noStore shows opposite of what the value is'); + assert.dom(SELECTORS.customTtlValue).containsText('10m', 'TTL shown as duration'); + }); + + test('it should render the notAfter date if present', async function (assert) { + assert.expect(1); + this.model = this.store.createRecord('pki/role', { + name: 'Foobar', + backend: 'pki', + noStore: false, + keyUsage: [], + extKeyUsage: ['bar', 'baz'], + notAfter: '2030-05-04T12:00:00.000Z', + }); + await render( + hbs` + + `, + { owner: this.engine } + ); + assert.dom(SELECTORS.customTtlValue).containsText('May', 'Formats the notAfter date instead of TTL'); }); });