UI: Transform secrets engine with transformations
* Ui/transform enable (#9647) * Show Transform on engines list if enterprise * Add box-radio component * Add is-disabled styling for box-radio and fix tooltip styling when position: above * Add KMIP and Transform to possible features on has feature helper * Sidebranch: Transform Secret Engine Initial setup (#9625) * WIP // list transforms, console.logs and all * setup LIST transformations ajax request and draft out options-for-backend options * change from plural to singluar and add transform to secret-edit * create two transform edit components * modify transform model with new attrs * add adapterFor to connect transform adapter to transform-edit-form component * setup Allowed roles searchSelect component to search over new transform/role adapter and model. * clean up for PR * clean up linting errors * restructure adapter call, now it works. * remove console * setup template model for SearchSelect component * add props to form field and search select for styling Co-authored-by: Chelsea Shaw <chelshaw.dev@gmail.com> * Ui/transform language fixes (#9666) * Update casing and wording on Transform list route. Use generic list item for transformations * Add back js file for transformation-edit * Set up transform for tabs * Ui/create edit transformation fixes (#9668) * add conditional for masking vs tweak source based on type, and update text for create transformation * change order * fix error with stringArray * setup the edit/delete transformation view * clean up toolbar links * setup serializer to change response of mask character from keycode to character * change styling of label and sub-text size, confirmed with design * temp fix on templates vs template * add clickable list item * add space between template list * setup styling and structure for the rest of the show transformation. TODO: turn into components. * create transform-show-transformation component * add attachCapabilities to transform model and update transform-transformation-itme list accordingly * clean up liniting errors * address pr comments * remove leftover * clean up * Sidebranch: UI transform create and edit clean up (#9778) * clean up some of the TODOs * setup edit view with read only attributes for name and template * setup initial selected for search select component * fixes * hide templates form field for now * set selectLimit for search select component * hide power select if the select limit is greater than or equal to the selectedOptions length * clean up failing linting * address pr comments * Ui/fix list roles transformation (#9788) * Update search-select to pass backend to query if exists * Update role and template adapters * cleanup * Fix replace with static string * Ui/transform cleanup 2 (#9789) * amend encode/decode commands for now until design gets back with more details * restrict character count on masking input field * clean up selectLimit * show backend instead of transform in cli copy command * Show KMIP un-selectable if enterprise but no ADP module (#9780) * New component transform-edit-base * Duplicate RoleEdit as TransformEditBase and swap in all transform components * Roll back role-edit changes * Update to transform edit base * Remove extraeneous set backend type on transform components * formatting * Revert search-select changes * Update template/templates data on transformation (#9838) Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com>
This commit is contained in:
parent
4c4fb54806
commit
5c64846225
|
@ -0,0 +1,98 @@
|
|||
import { assign } from '@ember/polyfills';
|
||||
import { allSettled } from 'rsvp';
|
||||
import ApplicationAdapter from './application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
createOrUpdate(store, type, snapshot) {
|
||||
const serializer = store.serializerFor(type.modelName);
|
||||
const data = serializer.serialize(snapshot);
|
||||
const { id } = snapshot;
|
||||
let url = this.urlForTransformations(snapshot.record.get('backend'), id);
|
||||
|
||||
return this.ajax(url, 'POST', { data });
|
||||
},
|
||||
|
||||
createRecord() {
|
||||
return this.createOrUpdate(...arguments);
|
||||
},
|
||||
|
||||
updateRecord() {
|
||||
return this.createOrUpdate(...arguments, 'update');
|
||||
},
|
||||
|
||||
deleteRecord(store, type, snapshot) {
|
||||
const { id } = snapshot;
|
||||
return this.ajax(this.urlForTransformations(snapshot.record.get('backend'), id), 'DELETE');
|
||||
},
|
||||
|
||||
pathForType() {
|
||||
return 'transform';
|
||||
},
|
||||
|
||||
urlForTransformations(backend, id) {
|
||||
let url = `${this.buildURL()}/${encodePath(backend)}/transformation`;
|
||||
if (id) {
|
||||
url = url + '/' + encodePath(id);
|
||||
}
|
||||
return url;
|
||||
},
|
||||
|
||||
optionsForQuery(id) {
|
||||
let data = {};
|
||||
if (!id) {
|
||||
data['list'] = true;
|
||||
}
|
||||
return { data };
|
||||
},
|
||||
|
||||
fetchByQuery(store, query) {
|
||||
const { id, backend } = query;
|
||||
const queryAjax = this.ajax(this.urlForTransformations(backend, id), 'GET', this.optionsForQuery(id));
|
||||
|
||||
return allSettled([queryAjax]).then(results => {
|
||||
// query result 404d, so throw the adapterError
|
||||
if (!results[0].value) {
|
||||
throw results[0].reason;
|
||||
}
|
||||
let resp = {
|
||||
id,
|
||||
name: id,
|
||||
backend,
|
||||
data: {},
|
||||
};
|
||||
|
||||
results.forEach(result => {
|
||||
if (result.value) {
|
||||
if (result.value.data.roles) {
|
||||
// TODO: Check if this is needed and remove if not
|
||||
resp.data = assign({}, resp.data, { zero_address_roles: result.value.data.roles });
|
||||
} else {
|
||||
let d = result.value.data;
|
||||
if (d.templates) {
|
||||
// In Transformations data goes up as "template", but comes down as "templates"
|
||||
// To keep the keys consistent we're translating here
|
||||
d = {
|
||||
...d,
|
||||
template: [d.templates],
|
||||
};
|
||||
delete d.templates;
|
||||
}
|
||||
resp.data = assign({}, resp.data, d);
|
||||
}
|
||||
}
|
||||
});
|
||||
return resp;
|
||||
});
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
return this.fetchByQuery(store, query);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import ApplicationAdapater from '../application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapater.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
pathForType() {
|
||||
return 'role';
|
||||
},
|
||||
|
||||
_url(backend, id) {
|
||||
let type = this.pathForType();
|
||||
let base = `/v1/${encodePath(backend)}/${type}`;
|
||||
if (id) {
|
||||
return `${base}/${encodePath(id)}`;
|
||||
}
|
||||
return base + '?list=true';
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.ajax(this._url(query.backend), 'GET').then(result => {
|
||||
return result;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import ApplicationAdapater from '../application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
|
||||
export default ApplicationAdapater.extend({
|
||||
namespace: 'v1',
|
||||
|
||||
pathForType() {
|
||||
return 'template';
|
||||
},
|
||||
|
||||
_url(backend, id) {
|
||||
let type = this.pathForType();
|
||||
let base = `${this.buildURL()}/${encodePath(backend)}/${type}`;
|
||||
if (id) {
|
||||
return `${base}/${encodePath(id)}`;
|
||||
}
|
||||
return base + '?list=true';
|
||||
},
|
||||
|
||||
query(store, type, query) {
|
||||
return this.ajax(this._url(query.backend), 'GET').then(result => {
|
||||
return result;
|
||||
});
|
||||
},
|
||||
});
|
|
@ -3,7 +3,7 @@ import { computed } from '@ember/object';
|
|||
import Component from '@ember/component';
|
||||
import { task } from 'ember-concurrency';
|
||||
import { methods } from 'vault/helpers/mountable-auth-methods';
|
||||
import { engines, KMIP } from 'vault/helpers/mountable-secret-engines';
|
||||
import { engines, KMIP, TRANSFORM } from 'vault/helpers/mountable-secret-engines';
|
||||
|
||||
const METHODS = methods();
|
||||
const ENGINES = engines();
|
||||
|
@ -56,11 +56,10 @@ export default Component.extend({
|
|||
}),
|
||||
|
||||
engines: computed('version.features[]', function() {
|
||||
if (this.version.hasFeature('KMIP')) {
|
||||
return ENGINES.concat([KMIP]);
|
||||
} else {
|
||||
return ENGINES;
|
||||
if (this.get('version.isEnterprise')) {
|
||||
return ENGINES.concat([KMIP, TRANSFORM]);
|
||||
}
|
||||
return ENGINES;
|
||||
}),
|
||||
|
||||
willDestroy() {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import TransformBase from './transform-edit-base';
|
||||
|
||||
export default TransformBase.extend({});
|
|
@ -0,0 +1,104 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import { or } from '@ember/object/computed';
|
||||
import { isBlank } from '@ember/utils';
|
||||
import { task, waitForEvent } from 'ember-concurrency';
|
||||
import Component from '@ember/component';
|
||||
import { set, get } from '@ember/object';
|
||||
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
|
||||
import keys from 'vault/lib/keycodes';
|
||||
|
||||
const LIST_ROOT_ROUTE = 'vault.cluster.secrets.backend.list-root';
|
||||
const SHOW_ROUTE = 'vault.cluster.secrets.backend.show';
|
||||
|
||||
export default Component.extend(FocusOnInsertMixin, {
|
||||
router: service(),
|
||||
wizard: service(),
|
||||
|
||||
mode: null,
|
||||
// TODO: Investigate if we need all of these
|
||||
emptyData: '{\n}',
|
||||
onDataChange() {},
|
||||
onRefresh() {},
|
||||
model: null,
|
||||
requestInFlight: or('model.isLoading', 'model.isReloading', 'model.isSaving'),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set('backendType', 'transform');
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
if (this.model && this.model.isError) {
|
||||
this.model.rollbackAttributes();
|
||||
}
|
||||
},
|
||||
|
||||
waitForKeyUp: task(function*() {
|
||||
while (true) {
|
||||
let event = yield waitForEvent(document.body, 'keyup');
|
||||
this.onEscape(event);
|
||||
}
|
||||
})
|
||||
.on('didInsertElement')
|
||||
.cancelOn('willDestroyElement'),
|
||||
|
||||
transitionToRoute() {
|
||||
this.get('router').transitionTo(...arguments);
|
||||
},
|
||||
|
||||
onEscape(e) {
|
||||
if (e.keyCode !== keys.ESC || this.get('mode') !== 'show') {
|
||||
return;
|
||||
}
|
||||
this.transitionToRoute(LIST_ROOT_ROUTE);
|
||||
},
|
||||
|
||||
hasDataChanges() {
|
||||
get(this, 'onDataChange')(get(this, 'model.hasDirtyAttributes'));
|
||||
},
|
||||
|
||||
persist(method, successCallback) {
|
||||
const model = get(this, 'model');
|
||||
return model[method]().then(() => {
|
||||
if (!get(model, 'isError')) {
|
||||
if (this.get('wizard.featureState') === 'role') {
|
||||
this.get('wizard').transitionFeatureMachine('role', 'CONTINUE', this.get('backendType'));
|
||||
}
|
||||
successCallback(model);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
createOrUpdate(type, event) {
|
||||
event.preventDefault();
|
||||
const modelId = this.get('model.id') || this.get('model.name'); // transform comes in as model.name
|
||||
// prevent from submitting if there's no key
|
||||
// maybe do something fancier later
|
||||
if (type === 'create' && isBlank(modelId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.persist('save', () => {
|
||||
this.hasDataChanges();
|
||||
this.transitionToRoute(SHOW_ROUTE, modelId);
|
||||
});
|
||||
},
|
||||
|
||||
setValue(key, event) {
|
||||
set(get(this, 'model'), key, event.target.checked);
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.get('onRefresh')();
|
||||
},
|
||||
|
||||
delete() {
|
||||
this.persist('destroyRecord', () => {
|
||||
this.hasDataChanges();
|
||||
this.transitionToRoute(LIST_ROOT_ROUTE);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import TransformBase from './transform-edit-base';
|
||||
|
||||
export default TransformBase.extend({});
|
|
@ -0,0 +1,3 @@
|
|||
import TransformBase from './transform-edit-base';
|
||||
|
||||
export default TransformBase.extend({});
|
|
@ -0,0 +1,3 @@
|
|||
import TransformBase from './transform-edit-base';
|
||||
|
||||
export default TransformBase.extend({});
|
|
@ -5,6 +5,15 @@ export const KMIP = {
|
|||
value: 'kmip',
|
||||
type: 'kmip',
|
||||
category: 'generic',
|
||||
requiredFeature: 'KMIP',
|
||||
};
|
||||
|
||||
export const TRANSFORM = {
|
||||
displayName: 'Transform',
|
||||
value: 'transform',
|
||||
type: 'transform',
|
||||
category: 'generic',
|
||||
requiredFeature: 'Transform Secrets Engine',
|
||||
};
|
||||
|
||||
const MOUNTABLE_SECRET_ENGINES = [
|
||||
|
|
|
@ -55,6 +55,55 @@ const SECRET_BACKENDS = {
|
|||
editComponent: 'role-ssh-edit',
|
||||
listItemPartial: 'partials/secret-list/ssh-role-item',
|
||||
},
|
||||
transform: {
|
||||
displayName: 'Transformation',
|
||||
navigateTree: false,
|
||||
listItemPartial: 'partials/secret-list/transform-transformation-item',
|
||||
tabs: [
|
||||
{
|
||||
name: 'transformations',
|
||||
label: 'Transformations',
|
||||
searchPlaceholder: 'Filter transformations',
|
||||
item: 'transformation',
|
||||
create: 'Create transformation',
|
||||
editComponent: 'transformation-edit',
|
||||
},
|
||||
// TODO: Add tabs as needed
|
||||
// {
|
||||
// name: 'roles',
|
||||
// modelPrefix: 'role/',
|
||||
// label: 'Roles',
|
||||
// searchPlaceholder: 'Filter roles',
|
||||
// item: 'roles',
|
||||
// create: 'Create role',
|
||||
// tab: 'role',
|
||||
// listItemPartial: 'partials/secret-list/item',
|
||||
// editComponent: 'transform-role-edit',
|
||||
// },
|
||||
// {
|
||||
// name: 'templates',
|
||||
// modelPrefix: 'template/',
|
||||
// label: 'Templates',
|
||||
// searchPlaceholder: 'Filter templates',
|
||||
// item: 'templates',
|
||||
// create: 'Create template',
|
||||
// tab: 'template',
|
||||
// listItemPartial: 'partials/secret-list/item',
|
||||
// editComponent: 'transform-template-edit',
|
||||
// },
|
||||
// {
|
||||
// name: 'alphabets',
|
||||
// modelPrefix: 'alphabet/',
|
||||
// label: 'Alphabets',
|
||||
// searchPlaceholder: 'Filter alphabets',
|
||||
// item: 'alphabets',
|
||||
// create: 'Create alphabet',
|
||||
// tab: 'alphabet',
|
||||
// listItemPartial: 'partials/secret-list/item',
|
||||
// editComponent: 'alphabet-edit',
|
||||
// },
|
||||
],
|
||||
},
|
||||
transit: {
|
||||
searchPlaceholder: 'Filter keys',
|
||||
item: 'key',
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import { helper as buildHelper } from '@ember/component/helper';
|
||||
|
||||
const SUPPORTED_SECRET_BACKENDS = ['aws', 'cubbyhole', 'generic', 'kv', 'pki', 'ssh', 'transit', 'kmip'];
|
||||
const SUPPORTED_SECRET_BACKENDS = [
|
||||
'aws',
|
||||
'cubbyhole',
|
||||
'generic',
|
||||
'kv',
|
||||
'pki',
|
||||
'ssh',
|
||||
'transit',
|
||||
'kmip',
|
||||
'transform',
|
||||
];
|
||||
|
||||
export function supportedSecretBackends() {
|
||||
return SUPPORTED_SECRET_BACKENDS;
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import { computed } from '@ember/object';
|
||||
import DS from 'ember-data';
|
||||
import { apiPath } from 'vault/macros/lazy-capabilities';
|
||||
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
|
||||
import attachCapabilities from 'vault/lib/attach-capabilities';
|
||||
|
||||
const { attr } = DS;
|
||||
|
||||
// these arrays define the order in which the fields will be displayed
|
||||
// see
|
||||
//https://www.vaultproject.io/api-docs/secret/transform#create-update-transformation
|
||||
const TYPES = [
|
||||
{
|
||||
value: 'fpe',
|
||||
displayName: 'Format Preserving Encryption (FPE)',
|
||||
},
|
||||
{
|
||||
value: 'masking',
|
||||
displayName: 'Masking',
|
||||
},
|
||||
];
|
||||
|
||||
const TWEAK_SOURCE = [
|
||||
{
|
||||
value: 'supplied',
|
||||
displayName: 'supplied',
|
||||
},
|
||||
{
|
||||
value: 'generated',
|
||||
displayName: 'generated',
|
||||
},
|
||||
{
|
||||
value: 'internal',
|
||||
displayName: 'internal',
|
||||
},
|
||||
];
|
||||
|
||||
const Model = DS.Model.extend({
|
||||
useOpenAPI: false,
|
||||
name: attr('string', {
|
||||
// TODO: make this required for making a transformation
|
||||
label: 'Name',
|
||||
fieldValue: 'id',
|
||||
readOnly: true,
|
||||
subText: 'The name for your transformation. This cannot be edited later.',
|
||||
}),
|
||||
type: attr('string', {
|
||||
defaultValue: 'fpe',
|
||||
label: 'Type',
|
||||
possibleValues: TYPES,
|
||||
subText:
|
||||
'Vault provides two types of transformations: Format Preserving Encryption (FPE) is reversible, while Masking is not. This cannot be edited later.',
|
||||
}),
|
||||
tweak_source: attr('string', {
|
||||
defaultValue: 'supplied',
|
||||
label: 'Tweak source',
|
||||
possibleValues: TWEAK_SOURCE,
|
||||
subText: `A tweak value is used when performing FPE transformations. This can be supplied, generated, or internal.`, // TODO: I do not include the link here. Need to figure out the best way to approach this.
|
||||
}),
|
||||
masking_character: attr('string', {
|
||||
characterLimit: 1,
|
||||
defaultValue: '*',
|
||||
label: 'Masking character',
|
||||
subText: 'Specify which character you’d like to mask your data.',
|
||||
}),
|
||||
template: attr('array', {
|
||||
editType: 'searchSelect',
|
||||
fallbackComponent: 'string-list',
|
||||
label: 'Template', // TODO: make this required for making a transformation
|
||||
models: ['transform/template'],
|
||||
selectLimit: 1,
|
||||
subLabel: 'Template Name',
|
||||
subText:
|
||||
'Templates allow Vault to determine what and how to capture the value to be transformed. Type to use an existing template or create a new one.',
|
||||
}),
|
||||
allowed_roles: attr('array', {
|
||||
editType: 'searchSelect',
|
||||
label: 'Allowed roles',
|
||||
fallbackComponent: 'string-list',
|
||||
models: ['transform/role'],
|
||||
subText: 'Search for an existing role, type a new role to create it, or use a wildcard (*).',
|
||||
}),
|
||||
transformAttrs: computed('type', function() {
|
||||
if (this.type === 'masking') {
|
||||
return ['name', 'type', 'masking_character', 'template', 'allowed_roles'];
|
||||
}
|
||||
return ['name', 'type', 'tweak_source', 'template', 'allowed_roles'];
|
||||
}),
|
||||
transformFieldAttrs: computed('transformAttrs', function() {
|
||||
return expandAttributeMeta(this, this.get('transformAttrs'));
|
||||
}),
|
||||
});
|
||||
|
||||
export default attachCapabilities(Model, {
|
||||
updatePath: apiPath`transform/transformation/${'id'}`,
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import DS from 'ember-data';
|
||||
|
||||
export default DS.Model.extend({});
|
|
@ -0,0 +1,3 @@
|
|||
import DS from 'ember-data';
|
||||
|
||||
export default DS.Model.extend({});
|
|
@ -117,6 +117,8 @@ Router.map(function() {
|
|||
// transit-specific routes
|
||||
this.route('actions-root', { path: '/actions/' });
|
||||
this.route('actions', { path: '/actions/*secret' });
|
||||
// transform-specific routes
|
||||
// TODO: add these
|
||||
});
|
||||
});
|
||||
this.route('policies', { path: '/policies/:type' }, function() {
|
||||
|
|
|
@ -56,6 +56,7 @@ export default Route.extend({
|
|||
let types = {
|
||||
transit: 'transit-key',
|
||||
ssh: 'role-ssh',
|
||||
transform: 'transform',
|
||||
aws: 'role-aws',
|
||||
pki: tab === 'certs' ? 'pki-certificate' : 'role-pki',
|
||||
// secret or secret-v2
|
||||
|
@ -70,6 +71,7 @@ export default Route.extend({
|
|||
const secret = this.secretParam() || '';
|
||||
const backend = this.enginePathParam();
|
||||
const backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||
|
||||
return hash({
|
||||
secret,
|
||||
secrets: this.store
|
||||
|
|
|
@ -71,6 +71,7 @@ export default Route.extend(UnloadModelRoute, {
|
|||
let types = {
|
||||
transit: 'transit-key',
|
||||
ssh: 'role-ssh',
|
||||
transform: 'transform',
|
||||
aws: 'role-aws',
|
||||
pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki',
|
||||
cubbyhole: 'secret',
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
if (payload.data.masking_character) {
|
||||
payload.data.masking_character = String.fromCharCode(payload.data.masking_character);
|
||||
}
|
||||
return this._super(store, primaryModelClass, payload, id, requestType);
|
||||
},
|
||||
|
||||
serialize() {
|
||||
let json = this._super(...arguments);
|
||||
if (json.template && Array.isArray(json.template)) {
|
||||
// Transformations should only ever have one template
|
||||
json.template = json.template[0];
|
||||
}
|
||||
return json;
|
||||
},
|
||||
});
|
|
@ -175,12 +175,13 @@ export default Service.extend({
|
|||
// Returns relevant information from OpenAPI
|
||||
// as determined by the expandOpenApiProps util
|
||||
getProps(helpUrl, backend) {
|
||||
// add name of thing you want
|
||||
debug(`Fetching schema properties for ${backend} from ${helpUrl}`);
|
||||
|
||||
return this.ajax(helpUrl, backend).then(help => {
|
||||
// paths is an array but it will have a single entry
|
||||
// for the scope we're in
|
||||
const path = Object.keys(help.openapi.paths)[0];
|
||||
const path = Object.keys(help.openapi.paths)[0]; // do this or look at name
|
||||
const pathInfo = help.openapi.paths[path];
|
||||
const params = pathInfo.parameters;
|
||||
let paramProp = {};
|
||||
|
@ -202,7 +203,9 @@ export default Service.extend({
|
|||
}
|
||||
|
||||
// TODO: handle post endpoints without requestBody
|
||||
const props = pathInfo.post.requestBody.content['application/json'].schema.properties;
|
||||
const props = pathInfo.post
|
||||
? pathInfo.post.requestBody.content['application/json'].schema.properties
|
||||
: {};
|
||||
// put url params (e.g. {name}, {role})
|
||||
// at the front of the props list
|
||||
const newProps = assign({}, paramProp, props);
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
color: $grey;
|
||||
margin: $size-7 0 0 0;
|
||||
}
|
||||
.box-radio-spacing {
|
||||
margin: $size-6 $size-3 $size-6 0;
|
||||
}
|
||||
.box-radio {
|
||||
box-sizing: border-box;
|
||||
flex-basis: 7rem;
|
||||
|
@ -32,6 +35,10 @@
|
|||
box-shadow: 0 0 0 1px $grey-light, $box-shadow-middle;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
input[type='radio'].radio {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
|
|
@ -44,12 +44,13 @@
|
|||
.ember-basic-dropdown-content--below.ember-basic-dropdown-content--right.tool-tip {
|
||||
@include css-top-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px));
|
||||
}
|
||||
.ember-basic-dropdown-content--above.tool-tip {
|
||||
@include css-bottom-arrow(8px, $grey, 1px, $grey-dark);
|
||||
margin-top: -8px;
|
||||
}
|
||||
.ember-basic-dropdown-content--above.ember-basic-dropdown-content--right.tool-tip {
|
||||
@include css-bottom-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px));
|
||||
}
|
||||
.ember-basic-dropdown-content--above.tool-tip {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.b-checkbox .tool-tip-trigger {
|
||||
position: relative;
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.copy-text {
|
||||
background: $ui-gray-010;
|
||||
|
||||
& > code {
|
||||
color: $ui-gray-800;
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
|
@ -103,6 +103,7 @@
|
|||
@import './components/token-expire-warning';
|
||||
@import './components/toolbar';
|
||||
@import './components/tool-tip';
|
||||
@import './components/transform-edit.scss';
|
||||
@import './components/transit-card';
|
||||
@import './components/ttl-picker2';
|
||||
@import './components/unseal-warning';
|
||||
|
|
|
@ -21,7 +21,7 @@ label {
|
|||
.b-checkbox .is-label {
|
||||
color: $grey-darker;
|
||||
display: inline-block;
|
||||
font-size: $size-small;
|
||||
font-size: $body-size;
|
||||
font-weight: $font-weight-bold;
|
||||
|
||||
&:not(:last-child) {
|
||||
|
@ -70,6 +70,11 @@ label {
|
|||
}
|
||||
}
|
||||
|
||||
.sub-text {
|
||||
color: $grey;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: $size-8;
|
||||
}
|
||||
.input,
|
||||
.textarea,
|
||||
.select select {
|
||||
|
@ -209,6 +214,12 @@ label {
|
|||
.field-body .field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.field {
|
||||
// cannot use :read-only selector because tag used for other purposes
|
||||
&.is-readOnly {
|
||||
background-color: $ui-gray-100;
|
||||
}
|
||||
}
|
||||
.field.has-addons {
|
||||
flex-wrap: wrap;
|
||||
.control {
|
||||
|
|
|
@ -168,6 +168,10 @@
|
|||
.has-top-margin-xl {
|
||||
margin-top: $spacing-xl;
|
||||
}
|
||||
.has-border-bottom-light {
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid $grey-light;
|
||||
}
|
||||
.has-border-danger {
|
||||
border: 1px solid $danger;
|
||||
}
|
||||
|
|
|
@ -49,34 +49,22 @@
|
|||
</h3>
|
||||
<div class="box-radio-container">
|
||||
{{#each (filter-by "category" category mountTypes) as |type|}}
|
||||
<label
|
||||
for={{type.type}}
|
||||
class="box-radio
|
||||
{{if (eq mountModel.type type.type) " is-selected"}}"
|
||||
data-test-mount-type-radio
|
||||
data-test-mount-type={{type.type}}
|
||||
>
|
||||
<Icon
|
||||
<BoxRadio
|
||||
@displayName={{type.displayName}}
|
||||
@type={{type.type}}
|
||||
@glyph={{or type.glyph type.type}}
|
||||
@size="xl"
|
||||
class="has-text-grey-light"
|
||||
/>
|
||||
|
||||
{{type.displayName}}
|
||||
<RadioButton
|
||||
@value={{type.type}}
|
||||
@radioClass="radio"
|
||||
@groupValue={{mountModel.type}}
|
||||
@changed={{queue
|
||||
@groupName="mount-type"
|
||||
@onRadioChange={{queue
|
||||
(action (mut mountModel.type))
|
||||
(action "onTypeChange" "type")
|
||||
}}
|
||||
@name="mount-type"
|
||||
@radioId={{type.type}}
|
||||
@disabled={{if type.requiredFeature (not (has-feature type.requiredFeature)) false}}
|
||||
@tooltipMessage={{if (or (eq type.type 'transform') (eq type.type 'kmip'))
|
||||
(concat type.displayName " is part of the Advanced Data Protection module, which is not included in your enterprise license.")
|
||||
'This secret engine is not included in your license.'
|
||||
}}
|
||||
/>
|
||||
|
||||
<label for={{type.type}}></label>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<form onsubmit={{action "createOrUpdate" "create"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
{{message-error model=model}}
|
||||
{{!-- TODO: figure out what this ?? --}}
|
||||
{{!-- <NamespaceReminder @mode={{mode}} @noun="SSH role" /> --}}
|
||||
{{#each model.transformFieldAttrs as |attr|}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={{buttonDisabled}}
|
||||
class="button is-primary"
|
||||
data-test-role-ssh-create=true
|
||||
>
|
||||
{{#if (eq mode 'create')}}
|
||||
Create transformation
|
||||
{{else if (eq mode 'edit')}}
|
||||
Save
|
||||
{{/if}}
|
||||
</button>
|
||||
{{#secret-link
|
||||
mode=(if (eq mode "create") "list" "show")
|
||||
class="button"
|
||||
secret=model.id
|
||||
}}
|
||||
Cancel
|
||||
{{/secret-link}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1 @@
|
|||
{{yield}}
|
|
@ -0,0 +1,60 @@
|
|||
<form onsubmit={{action "createOrUpdate" "create"}}>
|
||||
<div class="box is-sideless is-fullwidth is-marginless">
|
||||
{{message-error model=model}}
|
||||
{{!-- TODO: figure out what this ?? --}}
|
||||
{{!-- <NamespaceReminder @mode={{mode}} @noun="SSH role" /> --}}
|
||||
{{#each model.transformFieldAttrs as |attr|}}
|
||||
{{#if (or (eq attr.name 'name') (eq attr.name 'type')) }}
|
||||
<label for="{{attr.name}}" class="is-label">
|
||||
{{attr.options.label}}
|
||||
</label>
|
||||
{{#if attr.options.subText}}
|
||||
<p class="sub-text">{{attr.options.subText}}</p>
|
||||
{{/if}}
|
||||
{{#if attr.options.possibleValues}}
|
||||
<div class="control is-expanded field is-readOnly">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="{{attr.name}}" id="{{attr.name}}" disabled data-test-input={{attr.name}}>
|
||||
<option selected={{get model attr.name}} value={{get model attr.name}}>
|
||||
{{get model attr.name}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<input data-test-input={{attr.name}} id={{attr.name}} autocomplete="off" spellcheck="false"
|
||||
value={{or (get model attr.name) attr.options.defaultValue}} readonly class="field input is-readOnly" type={{attr.type}} />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<FormField
|
||||
data-test-field
|
||||
@attr={{attr}}
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="field is-grouped-split box is-fullwidth is-bottomless">
|
||||
<div class="control">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={{buttonDisabled}}
|
||||
class="button is-primary"
|
||||
data-test-role-ssh-create=true
|
||||
>
|
||||
{{#if (eq mode 'create')}}
|
||||
Create transformation
|
||||
{{else if (eq mode 'edit')}}
|
||||
Save
|
||||
{{/if}}
|
||||
</button>
|
||||
{{#secret-link
|
||||
mode=(if (eq mode "create") "list" "show")
|
||||
class="button"
|
||||
secret=model.id
|
||||
}}
|
||||
Cancel
|
||||
{{/secret-link}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,47 @@
|
|||
<div class="box is-fullwidth is-sideless is-paddingless is-marginless">
|
||||
{{#each model.transformFieldAttrs as |attr|}}
|
||||
{{#if (eq attr.type "object")}}
|
||||
{{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(stringify (get model attr.name))}}
|
||||
{{else}}
|
||||
{{info-table-row label=(capitalize (or attr.options.label (humanize (dasherize attr.name)))) value=(get model attr.name)}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="has-top-margin-xl has-bottom-margin-s">
|
||||
<label class="title has-border-bottom-light page-header">CLI Commands</label>
|
||||
<div class="has-bottom-margin-s">
|
||||
<h2 class="title is-6">Encode</h2>
|
||||
<div class="has-bottom-margin-s">
|
||||
<span class="helper-text has-text-grey">
|
||||
To test the encoding capability of your transformation, use the following command. It will output an encoded_value.
|
||||
</span>
|
||||
</div>
|
||||
<div class="copy-text level">
|
||||
{{#let "vault write <backend>/encode/<your role name> value=<enter your value here> tweak=<base-64-string>" as |copyEncodeCommand|}}
|
||||
<code>vault write <backend>/encode/<your role name> value=<enter your value here> tweak=<base-64 string></code>
|
||||
<CopyButton class="button is-transparent level-right" @clipboardText={{copyEncodeCommand}}
|
||||
@buttonType="button" @success={{action (set-flash-message 'Command copied!')}}>
|
||||
<Icon @size='l' @glyph="copy-action" aria-label="Copy" />
|
||||
</CopyButton>
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="title is-6">Decode</h2>
|
||||
<div class="has-bottom-margin-s">
|
||||
<span class="helper-text has-text-grey">
|
||||
To test decoding capability of your transformation, use the encoded_value in the following command. It should return your original input.
|
||||
</span>
|
||||
</div>
|
||||
<div class="copy-text level">
|
||||
{{#let "vault write <backend>/decode/<your role name> value=<enter your value here> tweak=<base-64-string>" as |copyDecodeCommand|}}
|
||||
<code>vault write <backend>/decode/<your role name> value=<enter your value here> tweak=<base-64 string></code>
|
||||
<CopyButton class="button is-transparent level-right" @clipboardText={{copyDecodeCommand}}
|
||||
@buttonType="button" @success={{action (set-flash-message 'Command copied!')}}>
|
||||
<Icon @size='l' @glyph="copy-action" aria-label="Copy" />
|
||||
</CopyButton>
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,64 @@
|
|||
<PageHeader as |p|>
|
||||
<p.top>
|
||||
{{key-value-header
|
||||
baseKey=model
|
||||
path="vault.cluster.secrets.backend.list"
|
||||
mode=mode
|
||||
root=root
|
||||
showCurrent=true
|
||||
}}
|
||||
</p.top>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-secret-header="true">
|
||||
{{#if (eq mode "create") }}
|
||||
Create Transformation
|
||||
{{else if (eq mode 'edit')}}
|
||||
Edit Transformation
|
||||
{{else}}
|
||||
Transformation <code>{{model.id}}</code>
|
||||
{{/if}}
|
||||
</h1>
|
||||
</p.levelLeft>
|
||||
</PageHeader>
|
||||
|
||||
{{#if (eq mode "show")}}
|
||||
<Toolbar>
|
||||
<ToolbarActions>
|
||||
{{!-- TODO: update these actions, show delete grey out if not allowed --}}
|
||||
{{#if (or model.canUpdate model.canDelete)}}
|
||||
<div class="toolbar-separator" />
|
||||
{{/if}}
|
||||
{{#if model.canDelete}}
|
||||
{{!-- TODO only allow deletion when not in use by a role --}}
|
||||
<ConfirmAction
|
||||
@buttonClasses="toolbar-link"
|
||||
@onConfirmAction={{action "delete"}}
|
||||
@confirmTitle="Are you sure?"
|
||||
@confirmMessage="This transformation is not in use by a role and can be deleted."
|
||||
>
|
||||
Delete transformation
|
||||
</ConfirmAction>
|
||||
{{/if}}
|
||||
{{#if model.canUpdate }}
|
||||
<ToolbarSecretLink
|
||||
@secret={{model.id}}
|
||||
@mode="edit"
|
||||
@data-test-edit-link=true
|
||||
@replace=true
|
||||
>
|
||||
Edit transformation
|
||||
</ToolbarSecretLink>
|
||||
{{/if}}
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq mode 'edit')}}
|
||||
<TransformEditForm @mode={{mode}} @model={{model}} />
|
||||
{{else if (eq mode 'create')}}
|
||||
<TransformCreateForm @mode={{mode}} @model={{model}} />
|
||||
{{else}}
|
||||
<TransformShowTransformation
|
||||
@model={{model}}
|
||||
/>
|
||||
{{/if}}
|
|
@ -0,0 +1,60 @@
|
|||
{{!-- TODO do not let click if !canRead --}}
|
||||
{{#linked-block
|
||||
"vault.cluster.secrets.backend.show"
|
||||
item.id
|
||||
class="list-item-row"
|
||||
data-test-secret-link=item.id
|
||||
encode=true
|
||||
queryParams=(secret-query-params backendModel.type)
|
||||
}}
|
||||
<div class="columns is-mobile">
|
||||
<div class="column is-10">
|
||||
<SecretLink
|
||||
@mode="show"
|
||||
@secret={{item.id}}
|
||||
@queryParams={{if (eq backendModel.type "transform") (query-params tab="actions") ""}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
<Icon
|
||||
@glyph='file-outline'
|
||||
@class="has-text-grey-light"/>
|
||||
{{if (eq item.id ' ') '(self)' (or item.keyWithoutParent item.id)}}
|
||||
</SecretLink>
|
||||
</div>
|
||||
<div class="column has-text-right">
|
||||
<PopupMenu name="secret-menu">
|
||||
<nav class="menu">
|
||||
<ul class="menu-list">
|
||||
{{#if (or item.versionPath.isLoading item.secretPath.isLoading)}}
|
||||
<li class="action">
|
||||
<button disabled type="button" class="link button is-loading is-transparent">
|
||||
loading
|
||||
</button>
|
||||
</li>
|
||||
{{else}}
|
||||
{{#if item.updatePath.canRead}}
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="show"
|
||||
@secret={{item.id}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
Details
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if item.updatePath.canUpdate}}
|
||||
<li class="action">
|
||||
<SecretLink
|
||||
@mode="edit"
|
||||
@secret={{item.id}}
|
||||
@class="has-text-black has-text-weight-semibold">
|
||||
Edit
|
||||
</SecretLink>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
{{/linked-block}}
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* @module BoxRadio
|
||||
* BoxRadio components are used to display options for a radio selection.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <BoxRadio @displayName="Catahoula Leopard" @type="catahoula" @glyph="dog" @groupValue="labrador" @groupName="my-favorite-dog" @onRadioChange={{handleRadioChange}} />
|
||||
* ```
|
||||
* @param {string} displayName - This is the string that will show on the box radio option.
|
||||
* @param {string} type - type is the key that the radio input will be identified by. Please use a value without spaces.
|
||||
* @param {string} glyph - glyph is the name of the icon that will be used in the box
|
||||
* @param {string} groupValue - The key of the radio option that is currently selected for this radio group
|
||||
* @param {string} groupName - The name (key) of the group that this radio option belongs to
|
||||
* @param {function} onRadioChange - This callback will trigger when the radio option is selected (if enabled)
|
||||
* @param {boolean} [disabled=false] - This parameter controls whether the radio option is selectable. If not, it will be grayed out and show a tooltip.
|
||||
* @param {string} [tooltipMessage=default] - The message that shows in the tooltip if the radio option is disabled
|
||||
*/
|
||||
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/box-radio';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
disabled: false,
|
||||
tooltipMessage: 'This option is not available to you at this time.',
|
||||
});
|
|
@ -21,7 +21,7 @@ import layout from '../templates/components/form-field';
|
|||
* @param model=null {DS.Model} - The Ember Data model that `attr` is defined on
|
||||
* @param [disabled=false] {Boolean} - whether the field is disabled
|
||||
* @param [showHelpText=true] {Boolean} - whether to show the tooltip with help text from OpenAPI
|
||||
*
|
||||
* @param [subText] {String} - Text to be displayed below the label
|
||||
*
|
||||
*/
|
||||
|
||||
|
@ -31,6 +31,7 @@ export default Component.extend({
|
|||
classNames: ['field'],
|
||||
disabled: false,
|
||||
showHelpText: true,
|
||||
subText: '',
|
||||
|
||||
onChange() {},
|
||||
|
||||
|
|
|
@ -9,15 +9,19 @@ import layout from '../templates/components/search-select';
|
|||
* @module SearchSelect
|
||||
* The `SearchSelect` is an implementation of the [ember-power-select-with-create](https://github.com/poteto/ember-cli-flash) used for form elements where options come dynamically from the API.
|
||||
* @example
|
||||
* <SearchSelect @id="group-policies" @models={{["policies/acl"]}} @onChange={{onChange}} @inputValue={{get model valuePath}} @helpText="Policies associated with this group" @label="Policies" @fallbackComponent="string-list" />
|
||||
* <SearchSelect @id="group-policies" @models={{["policies/acl"]}} @onChange={{onChange}} @selectLimit={{2}} @inputValue={{get model valuePath}} @helpText="Policies associated with this group" @label="Policies" @fallbackComponent="string-list" />
|
||||
*
|
||||
* @param id {String} - The name of the form field
|
||||
* @param models {String} - An array of model types to fetch from the API.
|
||||
* @param onChange {Func} - The onchange action for this form field.
|
||||
* @param inputValue {String | Array} - A comma-separated string or an array of strings.
|
||||
* @param [helpText] {String} - Text to be displayed in the info tooltip for this form field
|
||||
* @param [subText] {String} - Text to be displayed below the label
|
||||
* @param [selectLimit] {Number} - A number that sets the limit to how many select options they can choose
|
||||
* @param label {String} - Label for this form field
|
||||
* @param [subLabel] {String} - a smaller label below the main Label
|
||||
* @param fallbackComponent {String} - name of component to be rendered if the API call 403s
|
||||
* @param [backend] {String} - name of the backend if the query for options needs additional information (eg. secret backend)
|
||||
*
|
||||
* @param options {Array} - *Advanced usage* - `options` can be passed directly from the outside to the
|
||||
* power-select component. If doing this, `models` should not also be passed as that will overwrite the
|
||||
|
@ -86,7 +90,11 @@ export default Component.extend({
|
|||
this.set('shouldRenderName', true);
|
||||
}
|
||||
try {
|
||||
let options = yield this.store.query(modelType, {});
|
||||
let queryOptions = {};
|
||||
if (this.backend) {
|
||||
queryOptions = { backend: this.backend };
|
||||
}
|
||||
let options = yield this.store.query(modelType, queryOptions);
|
||||
this.formatOptions(options);
|
||||
} catch (err) {
|
||||
if (err.httpStatus === 404) {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { assert } from '@ember/debug';
|
|||
import Helper from '@ember/component/helper';
|
||||
import { observer } from '@ember/object';
|
||||
|
||||
const FEATURES = [
|
||||
const POSSIBLE_FEATURES = [
|
||||
'HSM',
|
||||
'Performance Replication',
|
||||
'DR Replication',
|
||||
|
@ -12,10 +12,12 @@ const FEATURES = [
|
|||
'Seal Wrapping',
|
||||
'Control Groups',
|
||||
'Namespaces',
|
||||
'KMIP',
|
||||
'Transform Secrets Engine',
|
||||
];
|
||||
|
||||
export function hasFeature(featureName, features) {
|
||||
if (!FEATURES.includes(featureName)) {
|
||||
if (!POSSIBLE_FEATURES.includes(featureName)) {
|
||||
assert(`${featureName} is not one of the available values for Vault Enterprise features.`, false);
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
{{#if disabled}}
|
||||
<div class="box-radio-spacing">
|
||||
<ToolTip
|
||||
@verticalPosition="above"
|
||||
@horizontalPosition="center"
|
||||
as |T|>
|
||||
<T.trigger @tabindex=false>
|
||||
<label
|
||||
for={{type}}
|
||||
class="box-radio is-disabled is-marginless"
|
||||
data-test-mount-type-radio
|
||||
data-test-mount-type={{type}}
|
||||
>
|
||||
<Icon
|
||||
@glyph={{glyph}}
|
||||
@size="xl"
|
||||
class="has-text-grey-light"
|
||||
/>
|
||||
{{displayName}}
|
||||
<RadioButton
|
||||
@value={{type}}
|
||||
@radioClass="radio"
|
||||
@groupValue={{groupValue}}
|
||||
@changed={{onRadioChange}}
|
||||
@name={{groupName}}
|
||||
@radioId={{type}}
|
||||
@disabled={{disabled}}
|
||||
/>
|
||||
<label for={{type}}></label>
|
||||
</label>
|
||||
</T.trigger>
|
||||
<T.content @class="tool-tip">
|
||||
<div class="box">
|
||||
{{tooltipMessage}}
|
||||
</div>
|
||||
</T.content>
|
||||
</ToolTip>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="box-radio-spacing">
|
||||
<label
|
||||
for={{type}}
|
||||
class="box-radio is-marginless
|
||||
{{if (eq groupValue type) " is-selected"}}"
|
||||
data-test-mount-type-radio
|
||||
data-test-mount-type={{type}}
|
||||
>
|
||||
<Icon
|
||||
@glyph={{glyph}}
|
||||
@size="xl"
|
||||
class="has-text-grey-light"
|
||||
/>
|
||||
{{displayName}}
|
||||
<RadioButton
|
||||
@value={{type}}
|
||||
@radioClass="radio"
|
||||
@groupValue={{mountType}}
|
||||
@changed={{onRadioChange}}
|
||||
@name={{groupName}}
|
||||
@radioId={{type}}
|
||||
@disabled={{disabled}}
|
||||
/>
|
||||
<label for={{type.type}}></label>
|
||||
</label>
|
||||
</div>
|
||||
{{/if}}
|
|
@ -26,6 +26,9 @@
|
|||
{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{#if attr.options.subText}}
|
||||
<p class="sub-text">{{attr.options.subText}}</p>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{#if attr.options.possibleValues}}
|
||||
<div class="control is-expanded">
|
||||
|
@ -64,10 +67,19 @@
|
|||
</div>
|
||||
{{else if (eq attr.options.editType "searchSelect")}}
|
||||
<div class="form-section">
|
||||
<SearchSelect @id={{attr.name}} @models={{attr.options.models}}
|
||||
@onChange={{action (action "setAndBroadcast" valuePath)}} @inputValue={{get model valuePath}}
|
||||
@helpText={{attr.options.helpText}} @label={{labelString}}
|
||||
@fallbackComponent={{attr.options.fallbackComponent}} />
|
||||
<SearchSelect
|
||||
@id={{attr.name}}
|
||||
@models={{attr.options.models}}
|
||||
@onChange={{action (action "setAndBroadcast" valuePath)}}
|
||||
@inputValue={{get model valuePath}}
|
||||
@helpText={{attr.options.helpText}}
|
||||
@subText={{attr.options.subText}}
|
||||
@label={{labelString}}
|
||||
@subLabel={{attr.options.subLabel}}
|
||||
@fallbackComponent={{attr.options.fallbackComponent}}
|
||||
@selectLimit={{attr.options.selectLimit}}
|
||||
@backend={{model.backend}}
|
||||
/>
|
||||
</div>
|
||||
{{else if (eq attr.options.editType "mountAccessor")}}
|
||||
{{mount-accessor-select
|
||||
|
@ -147,7 +159,7 @@
|
|||
value={{or (get model valuePath) attr.options.defaultValue}} oninput={{action
|
||||
(action "setAndBroadcast" valuePath)
|
||||
value="target.value"
|
||||
}} class="input" />
|
||||
}} class="input" maxLength={{attr.options.characterLimit}} />
|
||||
|
||||
{{#if attr.options.validationAttr}}
|
||||
{{#if
|
||||
|
|
|
@ -7,12 +7,19 @@
|
|||
helpText=helpText
|
||||
}}
|
||||
{{else}}
|
||||
<label class="{{if labelClass labelClass 'title is-4'}}" data-test-field-label>
|
||||
<label class="{{if labelClass labelClass 'title is-4'}}" data-test-field-label>
|
||||
{{label}}
|
||||
{{#if helpText}}
|
||||
{{#info-tooltip}}{{helpText}}{{/info-tooltip}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{#if subLabel}}
|
||||
<p class="is-label">{{subLabel}}</p>
|
||||
{{/if}}
|
||||
{{#if subText}}
|
||||
<p class="sub-text">{{subText}}</p>
|
||||
{{/if}}
|
||||
{{#unless (gte selectedOptions.length selectLimit)}}
|
||||
{{#power-select-with-create
|
||||
options=options
|
||||
search=search
|
||||
|
@ -34,6 +41,7 @@
|
|||
{{option.id}}
|
||||
{{/if}}
|
||||
{{/power-select-with-create}}
|
||||
{{/unless}}
|
||||
<ul class="search-select-list">
|
||||
{{#each selectedOptions as |selected|}}
|
||||
<li class="search-select-list-item" data-test-selected-option="true">
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'core/components/box-radio';
|
|
@ -0,0 +1,30 @@
|
|||
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/box-radio.js. To make changes, first edit that file and run "yarn gen-story-md box-radio" to re-generate the content.-->
|
||||
|
||||
## BoxRadio
|
||||
BoxRadio components are used to display options for a radio selection.
|
||||
|
||||
**Params**
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| displayName | <code>string</code> | | This is the string that will show on the box radio option. |
|
||||
| type | <code>string</code> | | type is the key that the radio input will be identified by. Please use a value without spaces. |
|
||||
| glyph | <code>string</code> | | glyph is the name of the icon that will be used in the box |
|
||||
| groupValue | <code>string</code> | | The key of the radio option that is currently selected for this radio group |
|
||||
| groupName | <code>string</code> | | The name (key) of the group that this radio option belongs to |
|
||||
| onRadioChange | <code>function</code> | | This callback will trigger when the radio option is selected (if enabled) |
|
||||
| [disabled] | <code>boolean</code> | <code>false</code> | This parameter controls whether the radio option is selectable. If not, it will be grayed out and show a tooltip. |
|
||||
| [tooltipMessage] | <code>string</code> | <code>"default"</code> | The message that shows in the tooltip if the radio option is disabled |
|
||||
|
||||
**Example**
|
||||
|
||||
```js
|
||||
<BoxRadio @displayName="Catahoula Leopard" @type="catahoula" @glyph="dog" @groupValue="labrador" @groupName="my-favorite-dog" @onRadioChange={{handleRadioChange}} />
|
||||
```
|
||||
|
||||
**See**
|
||||
|
||||
- [Uses of BoxRadio](https://github.com/hashicorp/vault/search?l=Handlebars&q=BoxRadio+OR+box-radio)
|
||||
- [BoxRadio Source Code](https://github.com/hashicorp/vault/blob/master/ui/lib/core/addon/components/box-radio.js)
|
||||
|
||||
---
|
|
@ -0,0 +1,50 @@
|
|||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { storiesOf } from '@storybook/ember';
|
||||
import { withKnobs, text, boolean, select } from '@storybook/addon-knobs';
|
||||
import notes from './box-radio.md';
|
||||
|
||||
const GLYPHS = {
|
||||
KMIP: 'kmip',
|
||||
Transform: 'transform',
|
||||
AWS: 'aws',
|
||||
Azure: 'azure',
|
||||
Cert: 'cert',
|
||||
GCP: 'gcp',
|
||||
Github: 'github',
|
||||
JWT: 'jwt',
|
||||
HashiCorp: 'hashicorp',
|
||||
LDAP: 'ldap',
|
||||
OKTA: 'okta',
|
||||
Radius: 'radius',
|
||||
Userpass: 'userpass',
|
||||
Secrets: 'kv',
|
||||
Consul: 'consul',
|
||||
};
|
||||
|
||||
storiesOf('BoxRadio', module)
|
||||
.addParameters({ options: { showPanel: true } })
|
||||
.addDecorator(withKnobs())
|
||||
.add(
|
||||
`BoxRadio`,
|
||||
() => ({
|
||||
template: hbs`
|
||||
<h5 class="title is-5">Box Radio</h5>
|
||||
<BoxRadio
|
||||
@type={{type}}
|
||||
@glyph={{type}}
|
||||
@displayName={{displayName}}
|
||||
@onRadioChange={{onRadioChange}}
|
||||
@disabled={{disabled}}
|
||||
/>
|
||||
`,
|
||||
context: {
|
||||
displayName: text('displayName', 'HashiCorp'),
|
||||
type: select('glyph', GLYPHS, 'hashicorp'),
|
||||
disabled: boolean('disabled', false),
|
||||
onRadioChange: e => {
|
||||
console.log('Radio changed!', e);
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ notes }
|
||||
);
|
|
@ -0,0 +1,16 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="10" height="10" rx="1" fill="white" stroke="#BAC1CC" stroke-width="2"/>
|
||||
<rect x="13" y="13" width="10" height="10" rx="1" fill="white" stroke="#BAC1CC" stroke-width="2"/>
|
||||
<rect x="19" y="6" width="2" height="2" fill="#BAC1CC"/>
|
||||
<rect x="19" y="9" width="2" height="2" fill="#BAC1CC"/>
|
||||
<rect x="11" y="21" width="2" height="2" transform="rotate(-180 11 21)" fill="#BAC1CC"/>
|
||||
<rect x="8" y="21" width="2" height="2" transform="rotate(-180 8 21)" fill="#BAC1CC"/>
|
||||
<rect x="5" y="21" width="2" height="2" transform="rotate(-180 5 21)" fill="#BAC1CC"/>
|
||||
<rect x="5" y="18" width="2" height="2" transform="rotate(-180 5 18)" fill="#BAC1CC"/>
|
||||
<rect x="5" y="15" width="2" height="2" transform="rotate(-180 5 15)" fill="#BAC1CC"/>
|
||||
<path d="M7.02393 9H8.78174L6.98438 2.65869H5.01123L3.21826 9H4.81787L5.12109 7.60693H6.72949L7.02393 9ZM5.89453 4.08691H5.97803L6.479 6.44678H5.37598L5.89453 4.08691Z" fill="#BAC1CC"/>
|
||||
<path d="M15.4248 17.3833L16.9761 18.1743L15.4248 18.9653L16.0269 20.0068L17.4902 19.062L17.3979 20.8022H18.6021L18.5098 19.0576L19.9731 20.0112L20.5752 18.9653L19.0195 18.1743L20.5752 17.3833L19.9731 16.3418L18.5142 17.291L18.6021 15.5464H17.3979L17.4858 17.291L16.0269 16.3374L15.4248 17.3833Z" fill="#BAC1CC"/>
|
||||
<rect x="13" y="3" width="2" height="2" fill="#BAC1CC"/>
|
||||
<rect x="16" y="3" width="2" height="2" fill="#BAC1CC"/>
|
||||
<rect x="19" y="3" width="2" height="2" fill="#BAC1CC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,50 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import sinon from 'sinon';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | box-radio', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
this.set('type', 'aws');
|
||||
this.set('displayName', 'An Option');
|
||||
this.set('mountType', '');
|
||||
this.set('disabled', false);
|
||||
});
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
const spy = sinon.spy();
|
||||
this.set('onRadioChange', spy);
|
||||
await render(hbs`<BoxRadio
|
||||
@type={{type}}
|
||||
@glyph={{type}}
|
||||
@displayName={{displayName}}
|
||||
@onRadioChange={{onRadioChange}}
|
||||
@disabled={{disabled}}
|
||||
/>`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), 'An Option', 'shows the display name of the option');
|
||||
assert.dom('.tooltip').doesNotExist('tooltip does not exist when disabled is false');
|
||||
await click('[data-test-mount-type="aws"]');
|
||||
assert.ok(spy.calledOnce, 'calls the radio change function when option clicked');
|
||||
});
|
||||
|
||||
test('it renders correctly when disabled', async function(assert) {
|
||||
const spy = sinon.spy();
|
||||
this.set('onRadioChange', spy);
|
||||
await render(hbs`<BoxRadio
|
||||
@type={{type}}
|
||||
@glyph={{type}}
|
||||
@displayName={{displayName}}
|
||||
@onRadioChange={{onRadioChange}}
|
||||
@disabled=true
|
||||
/>`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), 'An Option', 'shows the display name of the option');
|
||||
assert.dom('.ember-basic-dropdown-trigger').exists('tooltip exists');
|
||||
await click('[data-test-mount-type="aws"]');
|
||||
assert.ok(spy.notCalled, 'does not call the radio change function when option is clicked');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
module('Integration | Component | transform-edit-base', function(hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
// Set any properties with this.set('myProperty', 'value');
|
||||
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||
|
||||
await render(hbs`{{transform-edit-base}}`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), '');
|
||||
|
||||
// Template block usage:
|
||||
await render(hbs`
|
||||
{{#transform-edit-base}}
|
||||
template block text
|
||||
{{/transform-edit-base}}
|
||||
`);
|
||||
|
||||
assert.equal(this.element.textContent.trim(), 'template block text');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue