mirror of
https://github.com/hashicorp/vault.git
synced 2025-11-29 14:41:09 +01:00
UI: PKI Roles Edit (#18194)
This commit is contained in:
parent
bb99bfa3bd
commit
42e1ba2110
@ -24,6 +24,7 @@ export default class PkiRoleModel extends Model {
|
|||||||
@attr('string', {
|
@attr('string', {
|
||||||
label: 'Role name',
|
label: 'Role name',
|
||||||
fieldValue: 'name',
|
fieldValue: 'name',
|
||||||
|
editDisabled: true,
|
||||||
})
|
})
|
||||||
name;
|
name;
|
||||||
|
|
||||||
@ -50,7 +51,6 @@ export default class PkiRoleModel extends Model {
|
|||||||
helperTextEnabled:
|
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.',
|
'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',
|
editType: 'ttl',
|
||||||
hideToggle: true,
|
|
||||||
defaultValue: '30s', // The API type is "duration" which accepts both an integer and string e.g. 30 || '30s'
|
defaultValue: '30s', // The API type is "duration" which accepts both an integer and string e.g. 30 || '30s'
|
||||||
})
|
})
|
||||||
notBeforeDuration;
|
notBeforeDuration;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
@import 'ember-basic-dropdown';
|
@import 'ember-basic-dropdown';
|
||||||
@import 'ember-power-select';
|
@import 'ember-power-select';
|
||||||
@import './core';
|
@import './core';
|
||||||
|
@import './engines';
|
||||||
|
|
||||||
@mixin font-face($name) {
|
@mixin font-face($name) {
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|||||||
@ -287,3 +287,17 @@ ul.bullet {
|
|||||||
.has-text-align-center {
|
.has-text-align-center {
|
||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
2
ui/app/styles/engines.scss
Normal file
2
ui/app/styles/engines.scss
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// PKI Engine styles
|
||||||
|
@import './pki/pki-not-valid-after-form';
|
||||||
3
ui/app/styles/pki/pki-not-valid-after-form.scss
Normal file
3
ui/app/styles/pki/pki-not-valid-after-form.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.pki-radiogroup-label {
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
@ -61,6 +61,8 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{else if @formatDate}}
|
{{else if @formatDate}}
|
||||||
{{date-format @value @formatDate}}
|
{{date-format @value @formatDate}}
|
||||||
|
{{else if @formatTtl}}
|
||||||
|
{{this.formattedTtl}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if (eq @type "array")}}
|
{{#if (eq @type "array")}}
|
||||||
<InfoTableItemArray
|
<InfoTableItemArray
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { typeOf } from '@ember/utils';
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
|
import { convertFromSeconds, largestUnitFromSeconds } from 'core/utils/duration-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module InfoTableRow
|
* @module InfoTableRow
|
||||||
@ -56,6 +57,14 @@ export default class InfoTableRowComponent extends Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
get formattedTtl() {
|
||||||
|
const { value } = this.args;
|
||||||
|
if (Number.isInteger(value)) {
|
||||||
|
const unit = largestUnitFromSeconds(value);
|
||||||
|
return `${convertFromSeconds(value, unit)}${unit}`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
calculateLabelOverflow(el) {
|
calculateLabelOverflow(el) {
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s has-top-margin-negative-s">
|
|
||||||
<RadioButton
|
|
||||||
class="radio"
|
|
||||||
name="ttl"
|
|
||||||
@value="ttl"
|
|
||||||
@onChange={{this.onRadioButtonChange}}
|
|
||||||
@groupValue={{this.groupValue}}
|
|
||||||
data-test-radio-button="ttl"
|
|
||||||
/>
|
|
||||||
<label class="has-left-margin-xs">
|
|
||||||
<TtlPicker
|
|
||||||
data-test-input="ttl"
|
|
||||||
@onChange={{this.setAndBroadcastTtl}}
|
|
||||||
@label="TTL"
|
|
||||||
@helperTextEnabled={{@attr.options.helperTextEnabled}}
|
|
||||||
@description={{@attr.helpText}}
|
|
||||||
@time={{this.ttlTime}}
|
|
||||||
@unit="d"
|
|
||||||
@hideToggle={{true}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s">
|
|
||||||
<RadioButton
|
|
||||||
class="radio"
|
|
||||||
name="not_after"
|
|
||||||
@value="specificDate"
|
|
||||||
@onChange={{this.onRadioButtonChange}}
|
|
||||||
@groupValue={{this.groupValue}}
|
|
||||||
data-test-radio-button="not_after"
|
|
||||||
/>
|
|
||||||
<label class="has-left-margin-xs">
|
|
||||||
<span class="ttl-picker-label is-large">Specific date</span><br />
|
|
||||||
<p class="sub-text">
|
|
||||||
This value format should be given in UTC format YYYY-MM-ddTHH:MM:SSZ.
|
|
||||||
</p>
|
|
||||||
{{#if (eq this.groupValue "specificDate")}}
|
|
||||||
<input
|
|
||||||
id="not_after"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
value={{this.notAfter}}
|
|
||||||
{{on "input" this.setAndBroadcastInput}}
|
|
||||||
class="input"
|
|
||||||
data-test-input="not_after"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
@ -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|}}
|
|
||||||
* <RadioSelectTtlOrString @attr={{attr}} @model={{this.model}} />
|
|
||||||
* {{/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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -28,37 +28,12 @@ import Duration from '@icholy/duration';
|
|||||||
import { guidFor } from '@ember/object/internals';
|
import { guidFor } from '@ember/object/internals';
|
||||||
import Ember from 'ember';
|
import Ember from 'ember';
|
||||||
import { restartableTask, timeout } from 'ember-concurrency';
|
import { restartableTask, timeout } from 'ember-concurrency';
|
||||||
|
import {
|
||||||
export const secondsMap = {
|
convertFromSeconds,
|
||||||
s: 1,
|
convertToSeconds,
|
||||||
m: 60,
|
goSafeConvertFromSeconds,
|
||||||
h: 3600,
|
largestUnitFromSeconds,
|
||||||
d: 86400,
|
} from 'core/utils/duration-utils';
|
||||||
};
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
export default class TtlPickerComponent extends Component {
|
export default class TtlPickerComponent extends Component {
|
||||||
@tracked enableTTL = false;
|
@tracked enableTTL = false;
|
||||||
@tracked recalculateSeconds = false;
|
@tracked recalculateSeconds = false;
|
||||||
|
|||||||
52
ui/lib/core/addon/decorators/confirm-leave.js
Normal file
52
ui/lib/core/addon/decorators/confirm-leave.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
41
ui/lib/core/addon/utils/duration-utils.ts
Normal file
41
ui/lib/core/addon/utils/duration-utils.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
@ -1 +0,0 @@
|
|||||||
export { default } from 'core/components/radio-select-ttl-or-string';
|
|
||||||
1
ui/lib/core/app/decorators/confirm-leave.js
Normal file
1
ui/lib/core/app/decorators/confirm-leave.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { withConfirmLeave } from 'core/decorators/confirm-leave';
|
||||||
@ -50,7 +50,7 @@
|
|||||||
>
|
>
|
||||||
{{#if (gt val.length 0)}}
|
{{#if (gt val.length 0)}}
|
||||||
{{#each val as |key|}}
|
{{#each val as |key|}}
|
||||||
<span>{{key}},</span>
|
<span>{{key}}, </span>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
None
|
None
|
||||||
@ -62,14 +62,23 @@
|
|||||||
@value={{not val}}
|
@value={{not val}}
|
||||||
@alwaysRender={{true}}
|
@alwaysRender={{true}}
|
||||||
/>
|
/>
|
||||||
|
{{else if (eq attr.name "customTtl")}}
|
||||||
|
{{! Show either notAfter or ttl }}
|
||||||
|
<InfoTableRow
|
||||||
|
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||||
|
@value={{or @role.notAfter @role.ttl}}
|
||||||
|
@alwaysRender={{true}}
|
||||||
|
@formatDate={{if @role.notAfter "MMM d yyyy HH:mm zzzz"}}
|
||||||
|
@formatTtl={{@role.ttl}}
|
||||||
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<InfoTableRow
|
<InfoTableRow
|
||||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||||
@value={{val}}
|
@value={{val}}
|
||||||
@alwaysRender={{true}}
|
@alwaysRender={{true}}
|
||||||
@formatDate={{eq attr.name "customTtl"}}
|
|
||||||
@type={{or attr.type attr.options.type}}
|
@type={{or attr.type attr.options.type}}
|
||||||
@defaultShown={{attr.options.defaultShown}}
|
@defaultShown={{attr.options.defaultShown}}
|
||||||
|
@formatTtl={{eq attr.options.editType "ttl"}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
|
|||||||
56
ui/lib/pki/addon/components/pki-not-valid-after-form.hbs
Normal file
56
ui/lib/pki/addon/components/pki-not-valid-after-form.hbs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s has-top-margin-negative-s pki-radiogroup-label">
|
||||||
|
<RadioButton
|
||||||
|
id="ttlType"
|
||||||
|
class="radio"
|
||||||
|
name="notValidAfterOption"
|
||||||
|
@value="ttl"
|
||||||
|
@onChange={{this.onRadioButtonChange}}
|
||||||
|
@groupValue={{this.groupValue}}
|
||||||
|
data-test-radio-button="ttl"
|
||||||
|
/>
|
||||||
|
<div class="has-left-margin-xs">
|
||||||
|
<label class="has-left-margin-xs" for="ttlType" data-test-radio-label="ttl">
|
||||||
|
<span class="ttl-picker-label is-large">TTL</span>
|
||||||
|
</label>
|
||||||
|
{{#if (eq this.groupValue "ttl")}}
|
||||||
|
<TtlPicker
|
||||||
|
data-test-input="ttl"
|
||||||
|
@onChange={{this.setAndBroadcastTtl}}
|
||||||
|
@label="TTL"
|
||||||
|
@helperTextEnabled={{@attr.options.helperTextEnabled}}
|
||||||
|
@description={{@attr.helpText}}
|
||||||
|
@initialValue={{@model.ttl}}
|
||||||
|
@hideToggle={{true}}
|
||||||
|
>
|
||||||
|
<label class="sr-only" for="ttl">Set relative certificate expiry with TTL</label>
|
||||||
|
</TtlPicker>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow is-flex-center has-text-grey has-right-margin-s pki-radiogroup-label">
|
||||||
|
<RadioButton
|
||||||
|
id="dateType"
|
||||||
|
class="radio"
|
||||||
|
name="notValidAfterOption"
|
||||||
|
@value="specificDate"
|
||||||
|
@onChange={{this.onRadioButtonChange}}
|
||||||
|
@groupValue={{this.groupValue}}
|
||||||
|
data-test-radio-button="not_after"
|
||||||
|
/>
|
||||||
|
<div class="has-left-margin-xs">
|
||||||
|
<label class="ttl-picker-label is-large" for="dateType" data-test-radio-label="specificDate">Specific date</label>
|
||||||
|
{{#if (eq this.groupValue "specificDate")}}
|
||||||
|
<label class="sr-only" for="not_after">Set certificate expiry with specified date value</label>
|
||||||
|
<Input
|
||||||
|
id="not_after"
|
||||||
|
@type="date"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@value={{this.formDate}}
|
||||||
|
{{on "input" this.setAndBroadcastInput}}
|
||||||
|
class="input"
|
||||||
|
data-test-input="not_after"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
78
ui/lib/pki/addon/components/pki-not-valid-after-form.ts
Normal file
78
ui/lib/pki/addon/components/pki-not-valid-after-form.ts
Normal file
@ -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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <PkiNotValidAfterForm /> 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<Args> {
|
||||||
|
@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<HTMLInputElement>) {
|
||||||
|
const setDate = evt.target.valueAsDate?.toISOString();
|
||||||
|
if (!setDate) return;
|
||||||
|
|
||||||
|
this.cachedNotAfter = setDate;
|
||||||
|
this.args.model.notAfter = setDate;
|
||||||
|
this.formDate = this.calculateFormDate(setDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +1,14 @@
|
|||||||
<PageHeader as |p|>
|
<PageHeader as |p|>
|
||||||
<p.top>
|
<p.top>
|
||||||
<KeyValueHeader
|
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||||
@root={{hash label="role" text="role" path="vault.cluster.secrets.backend.pki.roles.index"}}
|
|
||||||
@isEngine={{true}}
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<span class="sep">
|
|
||||||
/
|
|
||||||
</span>
|
|
||||||
<LinkTo @route="roles.index">
|
|
||||||
{{@model.backend}}
|
|
||||||
</LinkTo>
|
|
||||||
</li>
|
|
||||||
</KeyValueHeader>
|
|
||||||
</p.top>
|
</p.top>
|
||||||
<p.levelLeft>
|
<p.levelLeft>
|
||||||
<h1 class="title is-3">
|
<h1 class="title is-3" data-test-role-details-title>
|
||||||
{{#if @model.isNew}}
|
{{#if @model.isNew}}
|
||||||
Create a PKI role
|
Create a PKI role
|
||||||
{{else}}
|
{{else}}
|
||||||
Edit a
|
Edit
|
||||||
{{@model.id}}
|
{{@model.name}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</h1>
|
</h1>
|
||||||
</p.levelLeft>
|
</p.levelLeft>
|
||||||
@ -43,7 +31,7 @@
|
|||||||
@modelValidations={{this.modelValidations}}
|
@modelValidations={{this.modelValidations}}
|
||||||
@showHelpText={{false}}
|
@showHelpText={{false}}
|
||||||
>
|
>
|
||||||
<RadioSelectTtlOrString @attr={{attr}} @model={{@model}} />
|
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
|
||||||
</FormField>
|
</FormField>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@ -27,6 +27,19 @@ export default class PkiRoleForm extends Component {
|
|||||||
@tracked invalidFormAlert;
|
@tracked invalidFormAlert;
|
||||||
@tracked modelValidations;
|
@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
|
@task
|
||||||
*save(event) {
|
*save(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
import { inject as service } from '@ember/service';
|
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 store;
|
||||||
@service secretMountPath;
|
@service secretMountPath;
|
||||||
@service pathHelp;
|
|
||||||
|
|
||||||
beforeModel() {
|
|
||||||
return this.pathHelp.getNewModel('pki/role', 'pki');
|
|
||||||
}
|
|
||||||
|
|
||||||
model() {
|
model() {
|
||||||
return this.store.createRecord('pki/role', {
|
return this.store.createRecord('pki/role', {
|
||||||
|
|||||||
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1 +1,5 @@
|
|||||||
roles.role.edit
|
<PkiRoleForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@onCancel={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.id}}
|
||||||
|
@onSave={{transition-to "vault.cluster.secrets.backend.pki.roles.role.details" this.model.id}}
|
||||||
|
/>
|
||||||
10
ui/tests/helpers/pki/pki-not-valid-after-form.js
Normal file
10
ui/tests/helpers/pki/pki-not-valid-after-form.js
Normal file
@ -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"]',
|
||||||
|
};
|
||||||
@ -6,4 +6,5 @@ export const SELECTORS = {
|
|||||||
noStoreValue: '[data-test-value-div="Store in storage backend"]',
|
noStoreValue: '[data-test-value-div="Store in storage backend"]',
|
||||||
keyUsageValue: '[data-test-value-div="Key usage"]',
|
keyUsageValue: '[data-test-value-div="Key usage"]',
|
||||||
extKeyUsageValue: '[data-test-value-div="Ext key usage"]',
|
extKeyUsageValue: '[data-test-value-div="Ext key usage"]',
|
||||||
|
customTtlValue: '[data-test-value-div="Issued certificates expire after"]',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { module, test } from 'qunit';
|
|||||||
import { resolve } from 'rsvp';
|
import { resolve } from 'rsvp';
|
||||||
import Service from '@ember/service';
|
import Service from '@ember/service';
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
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';
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
|
||||||
const VALUE = 'test value';
|
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');
|
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`<InfoTableRow
|
||||||
|
@label={{this.label}}
|
||||||
|
@value={{this.value}}
|
||||||
|
@formatTtl={{true}}
|
||||||
|
/>`);
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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`
|
||||||
|
<div class="has-top-margin-xxl">
|
||||||
|
<PkiNotValidAfterForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@attr={{this.attr}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
{ 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`
|
||||||
|
<div class="has-top-margin-xxl">
|
||||||
|
<PkiNotValidAfterForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@attr={{this.attr}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
{ 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`
|
||||||
|
<div class="has-top-margin-xxl">
|
||||||
|
<PkiNotValidAfterForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@attr={{this.attr}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
{ 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`
|
||||||
|
<div class="has-top-margin-xxl">
|
||||||
|
<PkiNotValidAfterForm
|
||||||
|
@model={{this.model}}
|
||||||
|
@attr={{this.attr}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
{ 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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`
|
|
||||||
<div class="has-top-margin-xxl">
|
|
||||||
<RadioSelectTtlOrString
|
|
||||||
@model={{this.model}}
|
|
||||||
@attr={{this.attr}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
{ 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`
|
|
||||||
<div class="has-top-margin-xxl">
|
|
||||||
<RadioSelectTtlOrString
|
|
||||||
@model={{this.model}}
|
|
||||||
@attr={{this.attr}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
{ 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.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -13,15 +13,16 @@ module('Integration | Component | pki role details page', function (hooks) {
|
|||||||
this.store = this.owner.lookup('service:store');
|
this.store = this.owner.lookup('service:store');
|
||||||
this.model = this.store.createRecord('pki/role', {
|
this.model = this.store.createRecord('pki/role', {
|
||||||
name: 'Foobar',
|
name: 'Foobar',
|
||||||
|
backend: 'pki',
|
||||||
noStore: false,
|
noStore: false,
|
||||||
keyUsage: [],
|
keyUsage: [],
|
||||||
extKeyUsage: ['bar', 'baz'],
|
extKeyUsage: ['bar', 'baz'],
|
||||||
|
ttl: 600,
|
||||||
});
|
});
|
||||||
this.model.backend = 'pki';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should render the page component', async function (assert) {
|
test('it should render the page component', async function (assert) {
|
||||||
assert.expect(7);
|
assert.expect(8);
|
||||||
await render(
|
await render(
|
||||||
hbs`
|
hbs`
|
||||||
<Page::PkiRoleDetails @role={{this.model}} />
|
<Page::PkiRoleDetails @role={{this.model}} />
|
||||||
@ -38,5 +39,25 @@ module('Integration | Component | pki role details page', function (hooks) {
|
|||||||
.dom(SELECTORS.extKeyUsageValue)
|
.dom(SELECTORS.extKeyUsageValue)
|
||||||
.hasText('bar, baz,', 'Key usage shows comma-joined values when array has items');
|
.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.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`
|
||||||
|
<Page::PkiRoleDetails @role={{this.model}} />
|
||||||
|
`,
|
||||||
|
{ owner: this.engine }
|
||||||
|
);
|
||||||
|
assert.dom(SELECTORS.customTtlValue).containsText('May', 'Formats the notAfter date instead of TTL');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user