UI - add kmip engine (#6936)

* add kmip engine

* adjust where kmip engine is mounted and sketch out routes

* add secret mount path service to share params to engines

* move list-controller and list-route mixins to core addon and adjust imports

* properly link kmip secrets from the secrets list page

* tweak routes and add list controllers

* stub out some models and adapters

* fix mixin exports

* move a bunch of components into the core addon

* use new empty yield in list-view in the namespace template

* scopes list using list-view and list-item components

* simplify and flatten routes, templates for all of the list pages

* role show route and template and scope create template

* add ember-router-helpers

* add more packages to the dependencies of the core addon

* add field-group-show component for listing fields from a model

* move more components to the shared addon

* make configure and configuration routes work and save a generated model

* save and list scopes

* role create, list, read

* list credentials properly

* move allowed attributes to field group

* show allowed operations on role details page

* add kmip logo to mount secrets engine list page

* add role edit page

* show all model attributes on role show page

* enable role edit

* fix newFields error by creating open api role model on the role list route

* only show selected fields on role edit page

* do not send scope and backend attrs to api

* move path-or-array to core addon

* move string-list component to core addon

* remove extra top border when there is only one field group

* add icons for all of the list pages

* update kmip config model so defaultValue doesn't error

* generate credentials

* credential create and show

* only show kmip when feature is enabled

* fix saving of TTL fields generated from Open API

* move masked-input and list-pagination components to core addon

* add param on edit form to allow for calling onSave after render happens

* polish credential show page and redirect there after generating credentials

* add externalLink for kmip engine

* add kmip-breadcrumb component

* use kmip-breadcrumb component

* add linkPrefix param to linked-block component to allow for routing programmatically inside an engine

* redirect to the right place when enabling kmip

* fix linting

* review feedback

* update signature for path-help usage

* fix ttl field expansion test

* remove role filed from role form, fix generate redirect

* remove field-group-show because it's in the core addon

* remove bottom rule from show pages

* fix Max TTL displayAttrs for ssh role

* update edit-form to take fields or attrs

* fix linting

* remove listenAddrs and set default val on ttl if a val is passed in
This commit is contained in:
Matthew Irish 2019-06-21 16:05:45 -05:00 committed by GitHub
parent 7c4eca5fb3
commit f0d7dc9a6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
139 changed files with 1544 additions and 145 deletions

View File

@ -218,7 +218,7 @@ func pathRoles(b *backend) *framework.Path {
The maximum allowed lease duration
`,
DisplayAttrs: &framework.DisplayAttributes{
Value: "Max TTL",
Name: "Max TTL",
},
},
"allowed_critical_options": &framework.FieldSchema{

View File

@ -0,0 +1,59 @@
import ApplicationAdapter from '../application';
import { encodePath } from 'vault/utils/path-encoding-helpers';
export default ApplicationAdapter.extend({
namespace: 'v1',
pathForType(type) {
return type.replace('kmip/', '');
},
_url(modelType, meta = {}, id) {
let { backend, scope, role } = meta;
let type = this.pathForType(modelType);
let base;
switch (type) {
case 'scope':
base = `${encodePath(backend)}/scope`;
break;
case 'role':
base = `${encodePath(backend)}/scope/${encodePath(scope)}/role`;
break;
case 'credential':
base = `${encodePath(backend)}/scope/${encodePath(scope)}/role/${encodePath(role)}/credential`;
break;
}
if (id && type === 'credential') {
return `/v1/${base}/lookup?serial_number=${encodePath(id)}`;
}
if (id) {
return `/v1/${base}/${encodePath(id)}`;
}
return `/v1/${base}`;
},
urlForQuery(query, modelType) {
let base = this._url(modelType, query);
return base + '?list=true';
},
query(store, type, query) {
return this.ajax(this.urlForQuery(query, type.modelName), 'GET');
},
queryRecord(store, type, query) {
let id = query.id;
delete query.id;
return this.ajax(this._url(type.modelName, query, id), 'GET').then(resp => {
resp.id = id;
resp = { ...resp, ...query };
return resp;
});
},
buildURL(modelName, id, snapshot, requestType, query) {
if (requestType === 'createRecord') {
return this._super(...arguments);
}
return this._super(`${modelName}`, id, snapshot, requestType, query);
},
});

View File

@ -0,0 +1,19 @@
import BaseAdapter from './base';
export default BaseAdapter.extend({
_url(id, modelName, snapshot) {
let name = this.pathForType(modelName);
// id here will be the mount path,
// modelName will be config so we want to transpose the first two call args
return this.buildURL(id, name, snapshot);
},
urlForFindRecord() {
return this._url(...arguments);
},
urlForCreateRecord(modelName, snapshot) {
return this._url(snapshot.id, modelName, snapshot);
},
urlForUpdateRecord() {
return this._url(...arguments);
},
});

View File

@ -0,0 +1,16 @@
import BaseAdapter from './base';
export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
let url = this._url(type.modelName, {
backend: snapshot.record.backend,
scope: snapshot.record.scope,
role: snapshot.record.role,
});
url = `${url}/generate`;
return this.ajax(url, 'POST', { data: snapshot.serialize() }).then(model => {
model.data.id = model.data.serial_number;
return model;
});
},
});

View File

@ -0,0 +1,25 @@
import BaseAdapter from './base';
export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
let name = snapshot.id || snapshot.attr('name');
let url = this._url(
type.modelName,
{
backend: snapshot.record.backend,
scope: snapshot.record.scope,
},
name
);
return this.ajax(url, 'POST', { data: snapshot.serialize() }).then(() => {
return {
id: name,
name,
};
});
},
updateRecord() {
return this.createRecord(...arguments);
},
});

View File

@ -0,0 +1,15 @@
import BaseAdapter from './base';
export default BaseAdapter.extend({
createRecord(store, type, snapshot) {
let name = snapshot.attr('name');
return this.ajax(this._url(type.modelName, { backend: snapshot.record.backend }, name), 'POST').then(
() => {
return {
id: name,
name,
};
}
);
},
});

View File

@ -28,6 +28,24 @@ App = Application.extend({
],
},
},
kmip: {
dependencies: {
services: [
'auth',
'flash-messages',
'namespace',
'path-help',
'router',
'store',
'version',
'wizard',
'secret-mount-path',
],
externalRoutes: {
secrets: 'vault.cluster.secrets.backends',
},
},
},
},
});

View File

@ -1,2 +0,0 @@
import OuterHTML from './outer-html';
export default OuterHTML.extend();

View File

@ -1,21 +0,0 @@
/**
* @module FieldGroupShow
* FieldGroupShow components loop through a Model's fieldGroups
* to display their attributes
*
* @example
* ```js
* <FieldGroupShow @model={{model}} @showAllFields=true />
* ```
*
* @param model {Object} - the model
* @param [showAllFields=false] {boolean} - whether to show fields with empty values
*/
import Component from '@ember/component';
import layout from '../templates/components/field-group-show';
export default Component.extend({
layout,
model: null,
showAllFields: false,
});

View File

@ -5,7 +5,7 @@ import d3Axis from 'd3-axis';
import d3TimeFormat from 'd3-time-format';
import { assign } from '@ember/polyfills';
import { computed } from '@ember/object';
import { run, debounce } from '@ember/runloop';
import { run } from '@ember/runloop';
import { task, waitForEvent } from 'ember-concurrency';
/**
@ -31,6 +31,8 @@ const HEIGHT = 240;
export default Component.extend({
classNames: ['http-requests-bar-chart-container'],
counters: null,
/* eslint-disable ember/avoid-leaking-state-in-ember-objects */
margin: { top: 24, right: 16, bottom: 24, left: 16 },
padding: 0.04,
width: 0,

View File

@ -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 } from 'vault/helpers/mountable-secret-engines';
import { engines, KMIP } from 'vault/helpers/mountable-secret-engines';
const METHODS = methods();
const ENGINES = engines();
@ -12,6 +12,7 @@ export default Component.extend({
store: service(),
wizard: service(),
flashMessages: service(),
version: service(),
/*
* @param Function
@ -51,7 +52,15 @@ export default Component.extend({
},
mountTypes: computed('mountType', function() {
return this.mountType === 'secret' ? ENGINES : METHODS;
return this.mountType === 'secret' ? this.engines : METHODS;
}),
engines: computed('version.features[]', function() {
if (this.version.hasFeature('KMIP')) {
return ENGINES.concat([KMIP]);
} else {
return ENGINES;
}
}),
willDestroy() {

View File

@ -1,5 +1,5 @@
import Controller from '@ember/controller';
import ListController from 'vault/mixins/list-controller';
import ListController from 'core/mixins/list-controller';
export default Controller.extend(ListController, {
actions: {

View File

@ -1,6 +1,6 @@
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import ListController from 'vault/mixins/list-controller';
import ListController from 'core/mixins/list-controller';
export default Controller.extend(ListController, {
flashMessages: service(),

View File

@ -2,7 +2,7 @@ import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import Controller, { inject as controller } from '@ember/controller';
import utils from 'vault/lib/key-utils';
import ListController from 'vault/mixins/list-controller';
import ListController from 'core/mixins/list-controller';
export default Controller.extend(ListController, {
flashMessages: service(),

View File

@ -4,7 +4,7 @@ import Controller from '@ember/controller';
import utils from 'vault/lib/key-utils';
import BackendCrumbMixin from 'vault/mixins/backend-crumb';
import WithNavToNearestAncestor from 'vault/mixins/with-nav-to-nearest-ancestor';
import ListController from 'vault/mixins/list-controller';
import ListController from 'core/mixins/list-controller';
export default Controller.extend(ListController, BackendCrumbMixin, WithNavToNearestAncestor, {
flashMessages: service(),

View File

@ -10,7 +10,11 @@ export default Controller.extend({
onMountSuccess: function(type, path) {
let transition;
if (SUPPORTED_BACKENDS.includes(type)) {
transition = this.transitionToRoute('vault.cluster.secrets.backend.index', path);
if (type === 'kmip') {
transition = this.transitionToRoute('vault.cluster.secrets.backend.kmip.scopes', path);
} else {
transition = this.transitionToRoute('vault.cluster.secrets.backend.index', path);
}
} else {
transition = this.transitionToRoute('vault.cluster.secrets.backends');
}

View File

@ -1,5 +1,12 @@
import { helper as buildHelper } from '@ember/component/helper';
export const KMIP = {
displayName: 'KMIP',
value: 'kmip',
type: 'kmip',
category: 'generic',
};
const MOUNTABLE_SECRET_ENGINES = [
{
displayName: 'Active Directory',

View File

@ -1,6 +1,6 @@
import { helper as buildHelper } from '@ember/component/helper';
const SUPPORTED_SECRET_BACKENDS = ['aws', 'cubbyhole', 'generic', 'kv', 'pki', 'ssh', 'transit'];
const SUPPORTED_SECRET_BACKENDS = ['aws', 'cubbyhole', 'generic', 'kv', 'pki', 'ssh', 'transit', 'kmip'];
export function supportedSecretBackends() {
return SUPPORTED_SECRET_BACKENDS;

View File

@ -1,12 +0,0 @@
import Mixin from '@ember/object/mixin';
export default Mixin.create({
queryParams: {
page: {
refreshModel: true,
},
pageFilter: {
refreshModel: true,
},
},
});

View File

@ -0,0 +1,18 @@
import DS from 'ember-data';
import { computed } from '@ember/object';
import { combineFieldGroups } from 'vault/utils/openapi-to-attrs';
import fieldToAttrs from 'vault/utils/field-to-attrs';
export default DS.Model.extend({
useOpenAPI: true,
getHelpUrl(path) {
return `/v1/${path}/config?help=1`;
},
fieldGroups: computed(function() {
let groups = [{ default: ['listenAddrs', 'connectionTimeout'] }];
groups = combineFieldGroups(groups, this.newFields, []);
return fieldToAttrs(this, groups);
}),
});

View File

@ -0,0 +1,30 @@
import DS from 'ember-data';
import fieldToAttrs from 'vault/utils/field-to-attrs';
import { computed } from '@ember/object';
const { attr } = DS;
export default DS.Model.extend({
backend: attr({ readOnly: true }),
scope: attr({ readOnly: true }),
role: attr({ readOnly: true }),
certificate: attr('string', { readOnly: true }),
caChain: attr({ readOnly: true }),
privateKey: attr('string', {
readOnly: true,
sensitive: true,
}),
format: attr('string', {
possibleValues: ['pem', 'der', 'pem_bundle'],
defaultValue: 'pem',
label: 'Certificate format',
}),
fieldGroups: computed(function() {
const groups = [
{
default: ['format'],
},
];
return fieldToAttrs(this, groups);
}),
});

View File

@ -0,0 +1,28 @@
import DS from 'ember-data';
import fieldToAttrs from 'vault/utils/field-to-attrs';
import { computed } from '@ember/object';
const { attr } = DS;
export default DS.Model.extend({
useOpenAPI: true,
backend: attr({ readOnly: true }),
scope: attr({ readOnly: true }),
getHelpUrl(path) {
return `/v1/${path}/scope/example/role/example?help=1`;
},
name: attr('string'),
allowedOperations: attr(),
fieldGroups: computed(function() {
let fields = this.newFields.without('role');
const groups = [
{
default: ['name'],
},
{ 'Allowed Operations': fields },
];
return fieldToAttrs(this, groups);
}),
});

View File

@ -0,0 +1,12 @@
import { computed } from '@ember/object';
import DS from 'ember-data';
const { attr } = DS;
import { expandAttributeMeta } from 'vault/utils/field-to-attrs';
export default DS.Model.extend({
name: attr('string'),
attrs: computed(function() {
return expandAttributeMeta(this, ['name']);
}),
});

View File

@ -83,6 +83,7 @@ Router.map(function() {
this.route('secrets', function() {
this.route('backends', { path: '/' });
this.route('backend', { path: '/:backend' }, function() {
this.mount('kmip');
this.route('index', { path: '/' });
this.route('configuration');
// because globs / params can't be empty,
@ -124,6 +125,7 @@ Router.map(function() {
if (config.addRootMounts) {
config.addRootMounts.call(this);
}
this.route('not-found', { path: '/*path' });
});
this.route('not-found', { path: '/*path' });

View File

@ -1,5 +1,5 @@
import Route from '@ember/routing/route';
import ListRoute from 'vault/mixins/list-route';
import ListRoute from 'core/mixins/list-route';
export default Route.extend(ListRoute, {
model(params) {

View File

@ -1,5 +1,5 @@
import Route from '@ember/routing/route';
import ListRoute from 'vault/mixins/list-route';
import ListRoute from 'core/mixins/list-route';
export default Route.extend(ListRoute, {
model(params) {

View File

@ -1,7 +1,7 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import ClusterRoute from 'vault/mixins/cluster-route';
import ListRoute from 'vault/mixins/list-route';
import ListRoute from 'core/mixins/list-route';
export default Route.extend(ClusterRoute, ListRoute, {
version: service(),

View File

@ -2,9 +2,11 @@ import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
export default Route.extend({
flashMessages: service(),
secretMountPath: service(),
oldModel: null,
model(params) {
let { backend } = params;
this.secretMountPath.update(backend);
return this.store
.query('secret-engine', {
path: backend,

View File

@ -197,7 +197,7 @@ export default Service.extend({
//we need list and create paths to set the correct urls for actions
const { list, create } = paths;
return generatedItemAdapter.extend({
urlForItem(method, id, type) {
urlForItem(method, id) {
let listPath = list.find(pathInfo => pathInfo.path.includes(itemType));
let { tag, path } = listPath;
let url = `${this.buildURL()}/${tag}/${backend}${path}/`;
@ -211,7 +211,7 @@ export default Service.extend({
return this.urlForItem(modelName, id, snapshot);
},
urlForUpdateRecord(id, modelName, snapshot) {
urlForUpdateRecord(id) {
let { tag, path } = create[0];
path = path.slice(0, path.indexOf('{') - 1);
return `${this.buildURL()}/${tag}/${backend}${path}/${id}`;
@ -224,7 +224,7 @@ export default Service.extend({
return `${this.buildURL()}/${tag}/${backend}${path}/${id}`;
},
urlForDeleteRecord(id, modelName, snapshot) {
urlForDeleteRecord(id) {
let { tag, path } = paths.delete[0];
path = path.slice(0, path.indexOf('{') - 1);
return `${this.buildURL()}/${tag}/${backend}${path}/${id}`;

View File

@ -0,0 +1,14 @@
import Service from '@ember/service';
// this service tracks the path of the currently viewed secret mount
// so that we can access that inside of engines where parent route params
// are not accessible
export default Service.extend({
currentPath: null,
update(path) {
this.set('currentPath', path);
},
get() {
return this.currentPath;
},
});

View File

@ -1,7 +0,0 @@
{{#link-to "vault.cluster.access.namespaces.create"}}
Create Namespace
{{/link-to}}
<LearnLink @path="/vault/security/namespaces">
Learn more
</LearnLink>

View File

@ -1,13 +0,0 @@
{{#if items.length}}
<div class="box is-fullwidth is-bottomless is-sideless is-paddingless">
{{#each items as |item|}}
{{yield (hash deleteItem=deleteItem saveItem=saveItem item=item)}}
{{/each}}
</div>
{{else}}
<EmptyState
@title={{this.emptyTitle}}
@message={{this.emptyMessage}}
@emptyActions={{this.emptyActions}}
/>
{{/if}}

View File

@ -18,42 +18,54 @@
</ToolbarActions>
</Toolbar>
<ListView @items={{model}} @itemNoun="namespace" @emptyActions="empty-action-namespaces" as |list|>
<ListItem as |Item|>
<Item.content>
{{list.item.id}}
</Item.content>
<Item.menu>
{{#with (concat currentNamespace (if currentNamespace "/") list.item.id) as |targetNamespace|}}
{{#if (contains targetNamespace accessibleNamespaces)}}
<li class="action">
{{#link-to "vault.cluster.secrets" (query-params namespace=targetNamespace) class="is-block"}}
Switch to Namespace
{{/link-to}}
</li>
{{/if}}
{{/with}}
<li class="action">
<ConfirmAction
@buttonClasses="link is-destroy"
@confirmButtonText="Remove"
@confirmMessage="Any engines or mounts in this namespace will also be removed."
@onConfirmAction={{action
(perform
Item.callMethod
"destroyRecord"
list.item
(concat "Successfully deleted namespace: " list.item.id)
"There was an error deleting this namespace: "
(action "refreshNamespaceList")
)
}}
>
Delete
</ConfirmAction>
</li>
</Item.menu>
</ListItem>
<ListView @items={{model}} @itemNoun="namespace" as |list|>
{{#if list.empty}}
<list.empty>
{{#link-to "vault.cluster.access.namespaces.create"}}
Create Namespace
{{/link-to}}
<LearnLink @path="/vault/security/namespaces">
Learn more
</LearnLink>
</list.empty>
{{else}}
<ListItem as |Item|>
<Item.content>
{{list.item.id}}
</Item.content>
<Item.menu>
{{#with (concat currentNamespace (if currentNamespace "/") list.item.id) as |targetNamespace|}}
{{#if (contains targetNamespace accessibleNamespaces)}}
<li class="action">
{{#link-to "vault.cluster.secrets" (query-params namespace=targetNamespace) class="is-block"}}
Switch to Namespace
{{/link-to}}
</li>
{{/if}}
{{/with}}
<li class="action">
<ConfirmAction
@buttonClasses="link is-destroy"
@confirmButtonText="Remove"
@confirmMessage="Any engines or mounts in this namespace will also be removed."
@onConfirmAction={{action
(perform
Item.callMethod
"destroyRecord"
list.item
(concat "Successfully deleted namespace: " list.item.id)
"There was an error deleting this namespace: "
(action "refreshNamespaceList")
)
}}
>
Delete
</ConfirmAction>
</li>
</Item.menu>
</ListItem>
{{/if}}
</ListView>
{{else}}
<UpgradePage @title="Namespaces" @minimumEdition="Vault Enterprise Pro" />

View File

@ -19,7 +19,10 @@
{{#each supportedBackends as |backend|}}
{{#linked-block
"vault.cluster.secrets.backend.list-root"
(if (eq backend.engineType "kmip")
"vault.cluster.secrets.backend.kmip.scopes"
"vault.cluster.secrets.backend.list-root"
)
backend.id
class="list-item-row"
data-test-secret-backend-row=backend.id
@ -30,7 +33,7 @@
<ToolTip @horizontalPosition="left" as |T|>
<T.trigger>
<Icon
@glyph={{or backend.engineType "secrets"}}
@glyph={{or (if (eq backend.engineType "kmip") "secrets" backend.engineType) "secrets"}}
@size="l"
class="has-text-grey-light"
/>
@ -41,7 +44,12 @@
</div>
</T.content>
</ToolTip>
{{#link-to "vault.cluster.secrets.backend.list-root" backend.id
{{#link-to
(if (eq backend.engineType "kmip")
"vault.cluster.secrets.backend.kmip.scopes"
"vault.cluster.secrets.backend.list-root"
)
backend.id
class="has-text-black has-text-weight-semibold"
data-test-secret-path=true
}}

View File

@ -23,7 +23,6 @@ export const expandOpenApiProps = function(props) {
}
let attrDefn = {
editType,
type: type,
helpText: description,
sensitive: sensitive,
label: name || label,
@ -33,6 +32,11 @@ export const expandOpenApiProps = function(props) {
readOnly: isId,
defaultValue: value || null,
};
// ttls write as a string and read as a number
// so setting type on them runs the wrong transform
if (editType !== 'ttl') {
attrDefn.type = type;
}
// loop to remove empty vals
for (let attrProp in attrDefn) {
if (attrDefn[attrProp] == null) {

View File

@ -2,8 +2,11 @@ import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import DS from 'ember-data';
import layout from '../templates/components/edit-form';
import { next } from '@ember/runloop';
export default Component.extend({
layout,
flashMessages: service(),
// public API
@ -22,6 +25,10 @@ export default Component.extend({
*/
onSave: () => {},
// onSave may need values updated in render in a helper - if this
// is the case, set this value to true
callOnSaveAfterRender: false,
save: task(function*(model, options = { method: 'save' }) {
let { method } = options;
let messageKey = method === 'save' ? 'successMessage' : 'deleteSuccessMessage';
@ -36,6 +43,12 @@ export default Component.extend({
return;
}
this.get('flashMessages').success(this.get(messageKey));
if (this.callOnSaveAfterRender) {
next(() => {
this.get('onSave')({ saveType: method, model });
});
return;
}
yield this.get('onSave')({ saveType: method, model });
})
.drop()

View File

@ -0,0 +1,20 @@
/**
* @module FieldGroupShow
* FieldGroupShow components are used to...
*
* @example
* ```js
* <FieldGroupShow @param1={param1} @param2={param2} />
* ```
*
* @param param1 {String} - param1 is...
* @param [param2=value] {String} - param2 is... //brackets mean it is optional and = sets the default value
*/
import Component from '@ember/component';
import layout from '../templates/components/field-group-show';
export default Component.extend({
layout,
model: null,
showAllFields: false,
});

View File

@ -1,5 +1,6 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import layout from '../templates/components/form-field-groups';
/**
* @module FormFieldGroups
@ -27,6 +28,7 @@ import { computed } from '@ember/object';
*/
export default Component.extend({
layout,
tagName: '',
renderGroup: computed(function() {

View File

@ -3,6 +3,7 @@ import { computed } from '@ember/object';
import { capitalize } from 'vault/helpers/capitalize';
import { humanize } from 'vault/helpers/humanize';
import { dasherize } from 'vault/helpers/dasherize';
import layout from '../templates/components/form-field';
/**
* @module FormField
@ -22,6 +23,7 @@ import { dasherize } from 'vault/helpers/dasherize';
*/
export default Component.extend({
layout,
'data-test-field': true,
classNames: ['field'],

View File

@ -1,6 +1,8 @@
import Component from '@ember/component';
import layout from '../templates/components/info-tooltip';
export default Component.extend({
layout,
'data-test-component': 'info-tooltip',
tagName: 'span',
classNames: ['is-inline-block'],

View File

@ -11,6 +11,7 @@ let LinkedBlockComponent = Component.extend({
classNames: 'linked-block',
queryParams: null,
linkPrefix: null,
encode: false,
@ -35,6 +36,11 @@ let LinkedBlockComponent = Component.extend({
if (queryParams) {
params.push({ queryParams });
}
if (this.linkPrefix) {
let targetRoute = this.params[0];
targetRoute = `${this.linkPrefix}.${targetRoute}`;
this.params[0] = targetRoute;
}
this.get('router').transitionTo(...params);
}
},

View File

@ -1,8 +1,10 @@
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { task } from 'ember-concurrency';
import layout from '../templates/components/list-item';
export default Component.extend({
layout,
flashMessages: service(),
tagName: '',
linkParams: null,

View File

@ -1,5 +1,7 @@
import Component from '@ember/component';
import layout from '../../templates/components/list-item/content';
export default Component.extend({
layout,
tagName: '',
});

View File

@ -1,6 +1,8 @@
import Component from '@ember/component';
import layout from '../../templates/components/list-item/popup-menu';
export default Component.extend({
layout,
tagName: '',
item: null,
hasMenu: null,

View File

@ -2,8 +2,10 @@ import { gt } from '@ember/object/computed';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { range } from 'ember-composable-helpers/helpers/range';
import layout from '../templates/components/list-pagination';
export default Component.extend({
layout,
classNames: ['box', 'is-shadowless', 'list-pagination'],
page: null,
lastPage: null,

View File

@ -1,14 +1,21 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { pluralize } from 'ember-inflector';
import layout from '../templates/components/list-view';
export default Component.extend({
layout,
tagName: '',
items: null,
itemNoun: 'item',
// the dasherized name of a component to render
// in the EmptyState component if there are no items in items.length
emptyActions: '',
showPagination: computed('paginationRouteName', 'items.meta{lastPage,total}', function() {
return this.paginationRouteName && this.items.meta.lastPage > 1 && this.items.meta.total > 0;
}),
paginationRouteName: '',
emptyTitle: computed('itemNoun', function() {
let items = pluralize(this.get('itemNoun'));

View File

@ -1,6 +1,7 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import autosize from 'autosize';
import layout from '../templates/components/masked-input';
/**
* @module MaskedInput
@ -24,6 +25,7 @@ import autosize from 'autosize';
*/
export default Component.extend({
layout,
value: null,
placeholder: 'value',
didInsertElement() {

View File

@ -2,8 +2,10 @@ import { inject as service } from '@ember/service';
import { not } from '@ember/object/computed';
import Component from '@ember/component';
import { computed } from '@ember/object';
import layout from '../templates/components/namespace-reminder';
export default Component.extend({
layout,
namespace: service(),
showMessage: not('namespace.inRootNamespace'),
//public API

View File

@ -1,12 +1,16 @@
import { schedule, debounce } from '@ember/runloop';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
//TODO MOVE THESE TO THE ADDON
import utils from 'vault/lib/key-utils';
import keys from 'vault/lib/keycodes';
import FocusOnInsertMixin from 'vault/mixins/focus-on-insert';
import { encodePath } from 'vault/utils/path-encoding-helpers';
const routeFor = function(type, mode) {
import layout from '../templates/components/navigate-input';
const routeFor = function(type, mode, urls) {
const MODES = {
secrets: 'vault.cluster.secrets.backend',
'secrets-cert': 'vault.cluster.secrets.backend',
@ -14,6 +18,11 @@ const routeFor = function(type, mode) {
'policy-list': 'vault.cluster.policies',
leases: 'vault.cluster.access.leases',
};
// urls object should have create, list, show keys
// so we'll return that here
if (urls) {
return urls[type.replace('-root', '')];
}
let useSuffix = true;
const typeVal = mode === 'secrets' || mode === 'leases' ? type : type.replace('-root', '');
const modeKey = mode + '-' + typeVal;
@ -26,9 +35,11 @@ const routeFor = function(type, mode) {
};
export default Component.extend(FocusOnInsertMixin, {
layout,
router: service(),
classNames: ['navigate-filter'],
urls: null,
// these get passed in from the outside
// actions that get passed in
@ -75,19 +86,25 @@ export default Component.extend(FocusOnInsertMixin, {
return;
}
if (this.get('filterMatchesKey') && !utils.keyIsFolder(val)) {
let params = [routeFor('show', mode), extraParams, this.keyForNav(val)].compact();
let params = [routeFor('show', mode, this.urls), extraParams, this.keyForNav(val)].compact();
this.transitionToRoute(...params);
} else {
if (mode === 'policies') {
return;
}
let route = routeFor('create', mode);
let route = routeFor('create', mode, this.urls);
if (baseKey) {
this.transitionToRoute(route, this.keyForNav(baseKey), {
queryParams: {
initialKey: val,
},
});
} else if (this.urls) {
this.transitionToRoute(route, {
queryParams: {
initialKey: this.keyForNav(val),
},
});
} else {
this.transitionToRoute(route + '-root', {
queryParams: {
@ -136,7 +153,7 @@ export default Component.extend(FocusOnInsertMixin, {
},
navigate(key, mode, pageFilter) {
const route = routeFor(key ? 'list' : 'list-root', mode);
const route = routeFor(key ? 'list' : 'list-root', mode, this.urls);
let args = [route];
if (key) {
args.push(key);
@ -161,7 +178,7 @@ export default Component.extend(FocusOnInsertMixin, {
filterUpdatedNoNav: function(val, mode) {
var key = val ? val.trim() : null;
this.transitionToRoute(routeFor('list-root', mode), {
this.transitionToRoute(routeFor('list-root', mode, this.urls), {
queryParams: {
pageFilter: key,
page: 1,

View File

@ -1,8 +1,10 @@
import ArrayProxy from '@ember/array/proxy';
import Component from '@ember/component';
import { set, computed } from '@ember/object';
import layout from '../templates/components/string-list';
export default Component.extend({
layout,
'data-test-component': 'string-list',
classNames: ['field', 'string-list', 'form-section'],

View File

@ -1,6 +1,8 @@
import HoverDropdown from 'ember-basic-dropdown-hover/components/basic-dropdown-hover';
import layout from '../templates/components/tool-tip';
export default HoverDropdown.extend({
layout,
delay: 0,
horizontalPosition: 'auto-right',
});

View File

@ -17,6 +17,10 @@
*
*/
import OuterHTML from './outer-html';
import Component from '@ember/component';
import layout from '../templates/components/toolbar-filters';
export default OuterHTML.extend({});
export default Component.extend({
layout,
tagName: '',
});

View File

@ -1,7 +1,7 @@
import { computed } from '@ember/object';
import Mixin from '@ember/object/mixin';
import escapeStringRegexp from 'escape-string-regexp';
import commonPrefix from 'vault/utils/common-prefix';
import commonPrefix from 'core/utils/common-prefix';
export default Mixin.create({
queryParams: {

View File

@ -0,0 +1,30 @@
import Mixin from '@ember/object/mixin';
import { get } from '@ember/object';
export default Mixin.create({
queryParams: {
page: {
refreshModel: true,
},
pageFilter: {
refreshModel: true,
},
},
setupController(controller, resolvedModel) {
let { pageFilter } = this.paramsFor(this.routeName);
this._super(...arguments);
controller.setProperties({
filter: pageFilter || '',
page: get(resolvedModel || {}, 'meta.currentPage') || 1,
});
},
resetController(controller, isExiting) {
this._super(...arguments);
if (isExiting) {
controller.set('pageFilter', null);
controller.set('filter', null);
}
},
});

View File

@ -18,9 +18,21 @@
<MessageError @model={{model}} data-test-edit-form-error />
<div class="box is-sideless is-fullwidth is-marginless">
<NamespaceReminder @mode="save" />
{{#each model.fields as |attr|}}
{{form-field data-test-field attr=attr model=model}}
{{/each}}
{{#if (or model.fields model.attrs)}}
{{#each (or model.fields model.attrs) as |attr|}}
<FormField
data-test-field
@attr={{attr}}
@model={{model}}
@mode={{@mode}}
/>
{{/each}}
{{else if model.fieldGroups}}
<FormFieldGroups
@model={{model}}
@mode={{@mode}}
/>
{{/if}}
</div>
<div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless">
<div class="field is-grouped">

View File

@ -3,11 +3,11 @@
{{#each-in fieldGroup as |group fields|}}
{{#if (or (eq group "default") (eq group "Options"))}}
{{#each fields as |attr|}}
{{#unless (eq attr.options.fieldValue 'id')}}
<InfoTableRow @alwaysRender={{true}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}} />
{{/unless}}
<InfoTableRow
@alwaysRender={{showAllFields}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}}
/>
{{/each}}
{{else}}
<div class="box {{unless showAllFields 'is-shadowless'}} is-sideless is-fullwidth is-marginless">
@ -15,12 +15,14 @@
{{group}}
</h2>
{{#each fields as |attr|}}
<InfoTableRow @alwaysRender={{showAllFields}}
<InfoTableRow
@alwaysRender={{showAllFields}}
@label={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@value={{get @model attr.name}} />
@value={{get @model attr.name}}
/>
{{/each}}
</div>
{{/if}}
{{/each-in}}
{{/each}}
</div>
</div>

View File

@ -100,7 +100,7 @@
initialValue=(or (get model valuePath) attr.options.defaultValue)
labelText=labelString
warning=attr.options.warning
setDefaultValue=(or attr.options.setDefault false)
setDefaultValue=(or (get model valuePath) attr.options.setDefault false)
onChange=(action (action "setAndBroadcast" valuePath))
}}
{{else if (eq attr.options.editType "stringArray")}}

View File

@ -1,7 +1,7 @@
{{#if componentName}}
{{component componentName item=item}}
{{else if linkParams}}
<LinkedBlock @params={{linkParams}} @class="list-item-row">
<LinkedBlock @params={{linkParams}} @linkPrefix={{@linkPrefix}} @class="list-item-row">
<div class="level is-mobile">
<div class="level-left is-flex-1">
{{#link-to params=linkParams class="has-text-weight-semibold has-text-black is-display-flex is-flex-1 is-no-underline"}}

View File

@ -0,0 +1 @@
{{yield}}

View File

@ -0,0 +1,23 @@
{{#if
(or
(and items.meta items.meta.total)
items.length
)
}}
<div class="box is-fullwidth is-bottomless is-sideless is-paddingless">
{{#each items as |item|}}
{{yield (hash item=item)}}
{{else}}
{{yield}}
{{/each}}
{{#if showPagination}}
<ListPagination
@page={{items.meta.currentPage}}
@lastPage={{items.meta.lastPage}}
@link={{@paginationRouteName}}
/>
{{/if}}
</div>
{{else}}
{{yield (hash empty=(component "empty-state" title=this.emptyTitle message=this.emptyMessage))}}
{{/if}}

View File

@ -0,0 +1 @@
export { default } from 'core/components/edit-form';

View File

@ -0,0 +1 @@
export { default } from 'core/components/field-group-show';

View File

@ -0,0 +1 @@
export { default } from 'core/components/form-field-groups';

View File

@ -0,0 +1 @@
export { default } from 'core/components/form-field';

View File

@ -0,0 +1 @@
export { default } from 'core/components/info-tooltip';

View File

@ -0,0 +1 @@
export { default } from 'core/components/linked-block';

View File

@ -0,0 +1 @@
export { default } from 'core/components/list-item';

View File

@ -0,0 +1 @@
export { default } from 'core/components/list-item/content';

View File

@ -0,0 +1 @@
export { default } from 'core/components/list-item/popup-menu';

View File

@ -0,0 +1 @@
export { default } from 'core/components/list-pagination';

View File

@ -0,0 +1 @@
export { default } from 'core/components/list-view';

View File

@ -0,0 +1 @@
export { default } from 'core/components/masked-input';

View File

@ -0,0 +1 @@
export { default } from 'core/components/namespace-reminder';

View File

@ -0,0 +1 @@
export { default } from 'core/components/navigate-input';

View File

@ -0,0 +1 @@
export { default } from 'core/components/string-list';

View File

@ -0,0 +1 @@
export { default } from 'core/components/tool-tip';

View File

@ -0,0 +1 @@
export { default } from 'core/components/toolbar-filters';

View File

@ -0,0 +1 @@
export { default } from 'core/helpers/path-or-array';

View File

@ -0,0 +1 @@
export { default } from 'core/mixins/list-controller';

View File

@ -0,0 +1 @@
export { default } from 'core/mixins/list-route';

View File

@ -4,10 +4,12 @@
"ember-addon"
],
"dependencies": {
"autosize": "*",
"Duration.js": "*",
"base64-js": "*",
"ember-auto-import": "*",
"ember-basic-dropdown": "*",
"ember-basic-dropdown-hover": "*",
"ember-cli-babel": "*",
"ember-cli-clipboard": "*",
"ember-cli-htmlbars": "*",
@ -17,7 +19,9 @@
"ember-concurrency": "*",
"ember-maybe-in-element": "*",
"ember-radio-button": "*",
"ember-router-helpers": "*",
"ember-svg-jar": "*",
"ember-truth-helpers": "*"
"ember-truth-helpers": "*",
"escape-string-regexp": "*"
}
}

View File

@ -0,0 +1,11 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import layout from '../templates/components/header-credentials';
export default Component.extend({
layout,
tagName: '',
secretMountPath: service(),
scope: null,
role: null,
});

View File

@ -0,0 +1,9 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import layout from '../templates/components/header-scope';
export default Component.extend({
layout,
tagName: '',
secretMountPath: service(),
});

View File

@ -0,0 +1,15 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import layout from '../templates/components/kmip-breadcrumb';
import { or } from '@ember/object/computed';
export default Component.extend({
layout,
tagName: '',
secretMountPath: service(),
shouldShowPath: or('showPath', 'scope', 'role'),
showPath: false,
path: null,
scope: null,
role: null,
});

View File

@ -0,0 +1,10 @@
import ListController from 'core/mixins/list-controller';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { getOwner } from '@ember/application';
export default Controller.extend(ListController, {
mountPoint: computed(function() {
return getOwner(this).mountPoint;
}),
});

View File

@ -0,0 +1,10 @@
import ListController from 'core/mixins/list-controller';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { getOwner } from '@ember/application';
export default Controller.extend(ListController, {
mountPoint: computed(function() {
return getOwner(this).mountPoint;
}),
});

View File

@ -0,0 +1,10 @@
import ListController from 'core/mixins/list-controller';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { getOwner } from '@ember/application';
export default Controller.extend(ListController, {
mountPoint: computed(function() {
return getOwner(this).mountPoint;
}),
});

Some files were not shown because too many files have changed in this diff Show More