mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 02:57:59 +00:00
UI: PKI Roles Edit (#18194)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import 'ember-basic-dropdown';
|
||||
@import 'ember-power-select';
|
||||
@import './core';
|
||||
@import './engines';
|
||||
|
||||
@mixin font-face($name) {
|
||||
@font-face {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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}}
|
||||
{{else if @formatDate}}
|
||||
{{date-format @value @formatDate}}
|
||||
{{else if @formatTtl}}
|
||||
{{this.formattedTtl}}
|
||||
{{else}}
|
||||
{{#if (eq @type "array")}}
|
||||
<InfoTableItemArray
|
||||
|
||||
@@ -2,6 +2,7 @@ import { typeOf } from '@ember/utils';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
import { convertFromSeconds, largestUnitFromSeconds } from 'core/utils/duration-utils';
|
||||
|
||||
/**
|
||||
* @module InfoTableRow
|
||||
@@ -56,6 +57,14 @@ export default class InfoTableRowComponent extends Component {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
get formattedTtl() {
|
||||
const { value } = this.args;
|
||||
if (Number.isInteger(value)) {
|
||||
const unit = largestUnitFromSeconds(value);
|
||||
return `${convertFromSeconds(value, unit)}${unit}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@action
|
||||
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 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;
|
||||
|
||||
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)}}
|
||||
{{#each val as |key|}}
|
||||
<span>{{key}},</span>
|
||||
<span>{{key}}, </span>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
None
|
||||
@@ -62,14 +62,23 @@
|
||||
@value={{not val}}
|
||||
@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}}
|
||||
<InfoTableRow
|
||||
@label={{capitalize (or attr.options.detailsLabel attr.options.label (humanize (dasherize attr.name)))}}
|
||||
@value={{val}}
|
||||
@alwaysRender={{true}}
|
||||
@formatDate={{eq attr.name "customTtl"}}
|
||||
@type={{or attr.type attr.options.type}}
|
||||
@defaultShown={{attr.options.defaultShown}}
|
||||
@formatTtl={{eq attr.options.editType "ttl"}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/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|>
|
||||
<p.top>
|
||||
<KeyValueHeader
|
||||
@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>
|
||||
<Page::Breadcrumbs @breadcrumbs={{this.breadcrumbs}} />
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3">
|
||||
<h1 class="title is-3" data-test-role-details-title>
|
||||
{{#if @model.isNew}}
|
||||
Create a PKI role
|
||||
{{else}}
|
||||
Edit a
|
||||
{{@model.id}}
|
||||
Edit
|
||||
{{@model.name}}
|
||||
{{/if}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
@@ -43,7 +31,7 @@
|
||||
@modelValidations={{this.modelValidations}}
|
||||
@showHelpText={{false}}
|
||||
>
|
||||
<RadioSelectTtlOrString @attr={{attr}} @model={{@model}} />
|
||||
<PkiNotValidAfterForm @attr={{attr}} @model={{@model}} />
|
||||
</FormField>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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"]',
|
||||
keyUsageValue: '[data-test-value-div="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 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`<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.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`
|
||||
<Page::PkiRoleDetails @role={{this.model}} />
|
||||
@@ -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`
|
||||
<Page::PkiRoleDetails @role={{this.model}} />
|
||||
`,
|
||||
{ owner: this.engine }
|
||||
);
|
||||
assert.dom(SELECTORS.customTtlValue).containsText('May', 'Formats the notAfter date instead of TTL');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user