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 Component from '@ember/component';
|
||||||
import { task } from 'ember-concurrency';
|
import { task } from 'ember-concurrency';
|
||||||
import { methods } from 'vault/helpers/mountable-auth-methods';
|
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 METHODS = methods();
|
||||||
const ENGINES = engines();
|
const ENGINES = engines();
|
||||||
|
@ -56,11 +56,10 @@ export default Component.extend({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
engines: computed('version.features[]', function() {
|
engines: computed('version.features[]', function() {
|
||||||
if (this.version.hasFeature('KMIP')) {
|
if (this.get('version.isEnterprise')) {
|
||||||
return ENGINES.concat([KMIP]);
|
return ENGINES.concat([KMIP, TRANSFORM]);
|
||||||
} else {
|
|
||||||
return ENGINES;
|
|
||||||
}
|
}
|
||||||
|
return ENGINES;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
willDestroy() {
|
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',
|
value: 'kmip',
|
||||||
type: 'kmip',
|
type: 'kmip',
|
||||||
category: 'generic',
|
category: 'generic',
|
||||||
|
requiredFeature: 'KMIP',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TRANSFORM = {
|
||||||
|
displayName: 'Transform',
|
||||||
|
value: 'transform',
|
||||||
|
type: 'transform',
|
||||||
|
category: 'generic',
|
||||||
|
requiredFeature: 'Transform Secrets Engine',
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOUNTABLE_SECRET_ENGINES = [
|
const MOUNTABLE_SECRET_ENGINES = [
|
||||||
|
|
|
@ -55,6 +55,55 @@ const SECRET_BACKENDS = {
|
||||||
editComponent: 'role-ssh-edit',
|
editComponent: 'role-ssh-edit',
|
||||||
listItemPartial: 'partials/secret-list/ssh-role-item',
|
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: {
|
transit: {
|
||||||
searchPlaceholder: 'Filter keys',
|
searchPlaceholder: 'Filter keys',
|
||||||
item: 'key',
|
item: 'key',
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
import { helper as buildHelper } from '@ember/component/helper';
|
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() {
|
export function supportedSecretBackends() {
|
||||||
return SUPPORTED_SECRET_BACKENDS;
|
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
|
// transit-specific routes
|
||||||
this.route('actions-root', { path: '/actions/' });
|
this.route('actions-root', { path: '/actions/' });
|
||||||
this.route('actions', { path: '/actions/*secret' });
|
this.route('actions', { path: '/actions/*secret' });
|
||||||
|
// transform-specific routes
|
||||||
|
// TODO: add these
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.route('policies', { path: '/policies/:type' }, function() {
|
this.route('policies', { path: '/policies/:type' }, function() {
|
||||||
|
|
|
@ -56,6 +56,7 @@ export default Route.extend({
|
||||||
let types = {
|
let types = {
|
||||||
transit: 'transit-key',
|
transit: 'transit-key',
|
||||||
ssh: 'role-ssh',
|
ssh: 'role-ssh',
|
||||||
|
transform: 'transform',
|
||||||
aws: 'role-aws',
|
aws: 'role-aws',
|
||||||
pki: tab === 'certs' ? 'pki-certificate' : 'role-pki',
|
pki: tab === 'certs' ? 'pki-certificate' : 'role-pki',
|
||||||
// secret or secret-v2
|
// secret or secret-v2
|
||||||
|
@ -70,6 +71,7 @@ export default Route.extend({
|
||||||
const secret = this.secretParam() || '';
|
const secret = this.secretParam() || '';
|
||||||
const backend = this.enginePathParam();
|
const backend = this.enginePathParam();
|
||||||
const backendModel = this.modelFor('vault.cluster.secrets.backend');
|
const backendModel = this.modelFor('vault.cluster.secrets.backend');
|
||||||
|
|
||||||
return hash({
|
return hash({
|
||||||
secret,
|
secret,
|
||||||
secrets: this.store
|
secrets: this.store
|
||||||
|
|
|
@ -71,6 +71,7 @@ export default Route.extend(UnloadModelRoute, {
|
||||||
let types = {
|
let types = {
|
||||||
transit: 'transit-key',
|
transit: 'transit-key',
|
||||||
ssh: 'role-ssh',
|
ssh: 'role-ssh',
|
||||||
|
transform: 'transform',
|
||||||
aws: 'role-aws',
|
aws: 'role-aws',
|
||||||
pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki',
|
pki: secret && secret.startsWith('cert/') ? 'pki-certificate' : 'role-pki',
|
||||||
cubbyhole: 'secret',
|
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
|
// Returns relevant information from OpenAPI
|
||||||
// as determined by the expandOpenApiProps util
|
// as determined by the expandOpenApiProps util
|
||||||
getProps(helpUrl, backend) {
|
getProps(helpUrl, backend) {
|
||||||
|
// add name of thing you want
|
||||||
debug(`Fetching schema properties for ${backend} from ${helpUrl}`);
|
debug(`Fetching schema properties for ${backend} from ${helpUrl}`);
|
||||||
|
|
||||||
return this.ajax(helpUrl, backend).then(help => {
|
return this.ajax(helpUrl, backend).then(help => {
|
||||||
// paths is an array but it will have a single entry
|
// paths is an array but it will have a single entry
|
||||||
// for the scope we're in
|
// 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 pathInfo = help.openapi.paths[path];
|
||||||
const params = pathInfo.parameters;
|
const params = pathInfo.parameters;
|
||||||
let paramProp = {};
|
let paramProp = {};
|
||||||
|
@ -202,7 +203,9 @@ export default Service.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: handle post endpoints without requestBody
|
// 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})
|
// put url params (e.g. {name}, {role})
|
||||||
// at the front of the props list
|
// at the front of the props list
|
||||||
const newProps = assign({}, paramProp, props);
|
const newProps = assign({}, paramProp, props);
|
||||||
|
|
|
@ -7,6 +7,9 @@
|
||||||
color: $grey;
|
color: $grey;
|
||||||
margin: $size-7 0 0 0;
|
margin: $size-7 0 0 0;
|
||||||
}
|
}
|
||||||
|
.box-radio-spacing {
|
||||||
|
margin: $size-6 $size-3 $size-6 0;
|
||||||
|
}
|
||||||
.box-radio {
|
.box-radio {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex-basis: 7rem;
|
flex-basis: 7rem;
|
||||||
|
@ -32,6 +35,10 @@
|
||||||
box-shadow: 0 0 0 1px $grey-light, $box-shadow-middle;
|
box-shadow: 0 0 0 1px $grey-light, $box-shadow-middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
input[type='radio'].radio {
|
input[type='radio'].radio {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
|
@ -44,12 +44,13 @@
|
||||||
.ember-basic-dropdown-content--below.ember-basic-dropdown-content--right.tool-tip {
|
.ember-basic-dropdown-content--below.ember-basic-dropdown-content--right.tool-tip {
|
||||||
@include css-top-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px));
|
@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 {
|
.ember-basic-dropdown-content--above.ember-basic-dropdown-content--right.tool-tip {
|
||||||
@include css-bottom-arrow(8px, $grey, 1px, $grey-dark, calc(100% - 20px));
|
@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 {
|
.b-checkbox .tool-tip-trigger {
|
||||||
position: relative;
|
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/token-expire-warning';
|
||||||
@import './components/toolbar';
|
@import './components/toolbar';
|
||||||
@import './components/tool-tip';
|
@import './components/tool-tip';
|
||||||
|
@import './components/transform-edit.scss';
|
||||||
@import './components/transit-card';
|
@import './components/transit-card';
|
||||||
@import './components/ttl-picker2';
|
@import './components/ttl-picker2';
|
||||||
@import './components/unseal-warning';
|
@import './components/unseal-warning';
|
||||||
|
|
|
@ -21,7 +21,7 @@ label {
|
||||||
.b-checkbox .is-label {
|
.b-checkbox .is-label {
|
||||||
color: $grey-darker;
|
color: $grey-darker;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: $size-small;
|
font-size: $body-size;
|
||||||
font-weight: $font-weight-bold;
|
font-weight: $font-weight-bold;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
|
@ -70,6 +70,11 @@ label {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sub-text {
|
||||||
|
color: $grey;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
font-size: $size-8;
|
||||||
|
}
|
||||||
.input,
|
.input,
|
||||||
.textarea,
|
.textarea,
|
||||||
.select select {
|
.select select {
|
||||||
|
@ -209,6 +214,12 @@ label {
|
||||||
.field-body .field {
|
.field-body .field {
|
||||||
margin-bottom: 0;
|
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 {
|
.field.has-addons {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
.control {
|
.control {
|
||||||
|
|
|
@ -168,6 +168,10 @@
|
||||||
.has-top-margin-xl {
|
.has-top-margin-xl {
|
||||||
margin-top: $spacing-xl;
|
margin-top: $spacing-xl;
|
||||||
}
|
}
|
||||||
|
.has-border-bottom-light {
|
||||||
|
border-radius: 0;
|
||||||
|
border-bottom: 1px solid $grey-light;
|
||||||
|
}
|
||||||
.has-border-danger {
|
.has-border-danger {
|
||||||
border: 1px solid $danger;
|
border: 1px solid $danger;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,34 +49,22 @@
|
||||||
</h3>
|
</h3>
|
||||||
<div class="box-radio-container">
|
<div class="box-radio-container">
|
||||||
{{#each (filter-by "category" category mountTypes) as |type|}}
|
{{#each (filter-by "category" category mountTypes) as |type|}}
|
||||||
<label
|
<BoxRadio
|
||||||
for={{type.type}}
|
@displayName={{type.displayName}}
|
||||||
class="box-radio
|
@type={{type.type}}
|
||||||
{{if (eq mountModel.type type.type) " is-selected"}}"
|
@glyph={{or type.glyph type.type}}
|
||||||
data-test-mount-type-radio
|
@groupValue={{mountModel.type}}
|
||||||
data-test-mount-type={{type.type}}
|
@groupName="mount-type"
|
||||||
>
|
@onRadioChange={{queue
|
||||||
<Icon
|
(action (mut mountModel.type))
|
||||||
@glyph={{or type.glyph type.type}}
|
(action "onTypeChange" "type")
|
||||||
@size="xl"
|
}}
|
||||||
class="has-text-grey-light"
|
@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.")
|
||||||
{{type.displayName}}
|
'This secret engine is not included in your license.'
|
||||||
<RadioButton
|
}}
|
||||||
@value={{type.type}}
|
/>
|
||||||
@radioClass="radio"
|
|
||||||
@groupValue={{mountModel.type}}
|
|
||||||
@changed={{queue
|
|
||||||
(action (mut mountModel.type))
|
|
||||||
(action "onTypeChange" "type")
|
|
||||||
}}
|
|
||||||
@name="mount-type"
|
|
||||||
@radioId={{type.type}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label for={{type.type}}></label>
|
|
||||||
</label>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{/each}}
|
{{/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 model=null {DS.Model} - The Ember Data model that `attr` is defined on
|
||||||
* @param [disabled=false] {Boolean} - whether the field is disabled
|
* @param [disabled=false] {Boolean} - whether the field is disabled
|
||||||
* @param [showHelpText=true] {Boolean} - whether to show the tooltip with help text from OpenAPI
|
* @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'],
|
classNames: ['field'],
|
||||||
disabled: false,
|
disabled: false,
|
||||||
showHelpText: true,
|
showHelpText: true,
|
||||||
|
subText: '',
|
||||||
|
|
||||||
onChange() {},
|
onChange() {},
|
||||||
|
|
||||||
|
|
|
@ -9,15 +9,19 @@ import layout from '../templates/components/search-select';
|
||||||
* @module SearchSelect
|
* @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.
|
* 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
|
* @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 id {String} - The name of the form field
|
||||||
* @param models {String} - An array of model types to fetch from the API.
|
* @param models {String} - An array of model types to fetch from the API.
|
||||||
* @param onChange {Func} - The onchange action for this form field.
|
* @param onChange {Func} - The onchange action for this form field.
|
||||||
* @param inputValue {String | Array} - A comma-separated string or an array of strings.
|
* @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 [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 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 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
|
* @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
|
* 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);
|
this.set('shouldRenderName', true);
|
||||||
}
|
}
|
||||||
try {
|
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);
|
this.formatOptions(options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.httpStatus === 404) {
|
if (err.httpStatus === 404) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { assert } from '@ember/debug';
|
||||||
import Helper from '@ember/component/helper';
|
import Helper from '@ember/component/helper';
|
||||||
import { observer } from '@ember/object';
|
import { observer } from '@ember/object';
|
||||||
|
|
||||||
const FEATURES = [
|
const POSSIBLE_FEATURES = [
|
||||||
'HSM',
|
'HSM',
|
||||||
'Performance Replication',
|
'Performance Replication',
|
||||||
'DR Replication',
|
'DR Replication',
|
||||||
|
@ -12,10 +12,12 @@ const FEATURES = [
|
||||||
'Seal Wrapping',
|
'Seal Wrapping',
|
||||||
'Control Groups',
|
'Control Groups',
|
||||||
'Namespaces',
|
'Namespaces',
|
||||||
|
'KMIP',
|
||||||
|
'Transform Secrets Engine',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function hasFeature(featureName, features) {
|
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);
|
assert(`${featureName} is not one of the available values for Vault Enterprise features.`, false);
|
||||||
return 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}}
|
{{/info-tooltip}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
|
{{#if attr.options.subText}}
|
||||||
|
<p class="sub-text">{{attr.options.subText}}</p>
|
||||||
|
{{/if}}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{#if attr.options.possibleValues}}
|
{{#if attr.options.possibleValues}}
|
||||||
<div class="control is-expanded">
|
<div class="control is-expanded">
|
||||||
|
@ -64,10 +67,19 @@
|
||||||
</div>
|
</div>
|
||||||
{{else if (eq attr.options.editType "searchSelect")}}
|
{{else if (eq attr.options.editType "searchSelect")}}
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<SearchSelect @id={{attr.name}} @models={{attr.options.models}}
|
<SearchSelect
|
||||||
@onChange={{action (action "setAndBroadcast" valuePath)}} @inputValue={{get model valuePath}}
|
@id={{attr.name}}
|
||||||
@helpText={{attr.options.helpText}} @label={{labelString}}
|
@models={{attr.options.models}}
|
||||||
@fallbackComponent={{attr.options.fallbackComponent}} />
|
@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>
|
</div>
|
||||||
{{else if (eq attr.options.editType "mountAccessor")}}
|
{{else if (eq attr.options.editType "mountAccessor")}}
|
||||||
{{mount-accessor-select
|
{{mount-accessor-select
|
||||||
|
@ -147,7 +159,7 @@
|
||||||
value={{or (get model valuePath) attr.options.defaultValue}} oninput={{action
|
value={{or (get model valuePath) attr.options.defaultValue}} oninput={{action
|
||||||
(action "setAndBroadcast" valuePath)
|
(action "setAndBroadcast" valuePath)
|
||||||
value="target.value"
|
value="target.value"
|
||||||
}} class="input" />
|
}} class="input" maxLength={{attr.options.characterLimit}} />
|
||||||
|
|
||||||
{{#if attr.options.validationAttr}}
|
{{#if attr.options.validationAttr}}
|
||||||
{{#if
|
{{#if
|
||||||
|
|
|
@ -7,33 +7,41 @@
|
||||||
helpText=helpText
|
helpText=helpText
|
||||||
}}
|
}}
|
||||||
{{else}}
|
{{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}}
|
{{label}}
|
||||||
{{#if helpText}}
|
{{#if helpText}}
|
||||||
{{#info-tooltip}}{{helpText}}{{/info-tooltip}}
|
{{#info-tooltip}}{{helpText}}{{/info-tooltip}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</label>
|
</label>
|
||||||
{{#power-select-with-create
|
{{#if subLabel}}
|
||||||
options=options
|
<p class="is-label">{{subLabel}}</p>
|
||||||
search=search
|
{{/if}}
|
||||||
onchange=(action "selectOption")
|
{{#if subText}}
|
||||||
oncreate=(action "createOption")
|
<p class="sub-text">{{subText}}</p>
|
||||||
placeholderComponent=(component "search-select-placeholder")
|
{{/if}}
|
||||||
renderInPlace=true
|
{{#unless (gte selectedOptions.length selectLimit)}}
|
||||||
searchField="searchText"
|
{{#power-select-with-create
|
||||||
verticalPosition="below"
|
options=options
|
||||||
showCreateWhen=(action "hideCreateOptionOnSameID")
|
search=search
|
||||||
buildSuggestion=(action "constructSuggestion") as |option|
|
onchange=(action "selectOption")
|
||||||
}}
|
oncreate=(action "createOption")
|
||||||
{{#if shouldRenderName}}
|
placeholderComponent=(component "search-select-placeholder")
|
||||||
{{option.name}}
|
renderInPlace=true
|
||||||
<small class="search-select-list-key" data-test-smaller-id="true">
|
searchField="searchText"
|
||||||
|
verticalPosition="below"
|
||||||
|
showCreateWhen=(action "hideCreateOptionOnSameID")
|
||||||
|
buildSuggestion=(action "constructSuggestion") as |option|
|
||||||
|
}}
|
||||||
|
{{#if shouldRenderName}}
|
||||||
|
{{option.name}}
|
||||||
|
<small class="search-select-list-key" data-test-smaller-id="true">
|
||||||
|
{{option.id}}
|
||||||
|
</small>
|
||||||
|
{{else}}
|
||||||
{{option.id}}
|
{{option.id}}
|
||||||
</small>
|
{{/if}}
|
||||||
{{else}}
|
{{/power-select-with-create}}
|
||||||
{{option.id}}
|
{{/unless}}
|
||||||
{{/if}}
|
|
||||||
{{/power-select-with-create}}
|
|
||||||
<ul class="search-select-list">
|
<ul class="search-select-list">
|
||||||
{{#each selectedOptions as |selected|}}
|
{{#each selectedOptions as |selected|}}
|
||||||
<li class="search-select-list-item" data-test-selected-option="true">
|
<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