mirror of https://github.com/status-im/consul.git
ui: KV Form and List Components (#8307)
* Add components for KV form, KV list and Session form * Pass through a @label attribute for a human label + don't require error * Ignore transition aborted errors for if you are re-transitioning * Make old confirmation dialog more ember-like and tagless * Make sure data-source and data-sink supports KV and sessions * Use new components and delete all the things * Fix up tests * Make list component tagless * Add component pageobject and fixup tests from that * Add eslint warning back in
This commit is contained in:
parent
ce6481d93d
commit
fce4311f55
|
@ -1,11 +1,13 @@
|
||||||
|
<div class={{concat "with-confirmation" (if confirming " confirming" "")}} ...attributes>
|
||||||
{{yield}}
|
{{yield}}
|
||||||
<YieldSlot @name="action" @params={{block-params confirm cancel}}>
|
<YieldSlot @name="action" @params={{block-params (action "confirm") (action "cancel")}}>
|
||||||
{{#if (or permanent (not confirming))}}
|
{{#if (or permanent (not confirming))}}
|
||||||
{{yield}}
|
{{yield}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</YieldSlot>
|
</YieldSlot>
|
||||||
<YieldSlot @name="dialog" @params={{block-params execute cancel message actionName}}>
|
<YieldSlot @name="dialog" @params={{block-params (action "execute") (action "cancel") message actionName}}>
|
||||||
{{#if confirming }}
|
{{#if confirming }}
|
||||||
{{yield}}
|
{{yield}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</YieldSlot>
|
</YieldSlot>
|
||||||
|
</div>
|
|
@ -1,31 +1,27 @@
|
||||||
/*eslint ember/closure-actions: "warn"*/
|
/*eslint ember/closure-actions: "warn"*/
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
|
|
||||||
import SlotsMixin from 'block-slots';
|
import Slotted from 'block-slots';
|
||||||
import { set } from '@ember/object';
|
import { set } from '@ember/object';
|
||||||
|
|
||||||
const cancel = function() {
|
export default Component.extend(Slotted, {
|
||||||
set(this, 'confirming', false);
|
tagName: '',
|
||||||
};
|
|
||||||
const execute = function() {
|
|
||||||
this.sendAction(...['actionName', ...this['arguments']]);
|
|
||||||
};
|
|
||||||
const confirm = function() {
|
|
||||||
const [action, ...args] = arguments;
|
|
||||||
set(this, 'actionName', action);
|
|
||||||
set(this, 'arguments', args);
|
|
||||||
set(this, 'confirming', true);
|
|
||||||
};
|
|
||||||
export default Component.extend(SlotsMixin, {
|
|
||||||
classNameBindings: ['confirming'],
|
|
||||||
classNames: ['with-confirmation'],
|
|
||||||
message: 'Are you sure?',
|
message: 'Are you sure?',
|
||||||
confirming: false,
|
confirming: false,
|
||||||
permanent: false,
|
permanent: false,
|
||||||
init: function() {
|
actions: {
|
||||||
this._super(...arguments);
|
cancel: function() {
|
||||||
this.cancel = cancel.bind(this);
|
set(this, 'confirming', false);
|
||||||
this.execute = execute.bind(this);
|
},
|
||||||
this.confirm = confirm.bind(this);
|
execute: function() {
|
||||||
|
set(this, 'confirming', false);
|
||||||
|
this.sendAction(...['actionName', ...this['arguments']]);
|
||||||
|
},
|
||||||
|
confirm: function() {
|
||||||
|
const [action, ...args] = arguments;
|
||||||
|
set(this, 'actionName', action);
|
||||||
|
set(this, 'arguments', args);
|
||||||
|
set(this, 'confirming', true);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<DataForm
|
||||||
|
@dc={{dc}}
|
||||||
|
@nspace={{nspace}}
|
||||||
|
@type="kv"
|
||||||
|
@label="key"
|
||||||
|
@autofill={{autofill}}
|
||||||
|
@item={{item}}
|
||||||
|
@src={{src}}
|
||||||
|
@onchange={{action "change"}}
|
||||||
|
@onsubmit={{action onsubmit}}
|
||||||
|
as |api|
|
||||||
|
>
|
||||||
|
<BlockSlot @name="content">
|
||||||
|
<form onsubmit={{action api.submit}}>
|
||||||
|
<fieldset disabled={{api.disabled}}>
|
||||||
|
{{#if api.isCreate}}
|
||||||
|
<label class="type-text{{if api.data.error.Key ' has-error'}}">
|
||||||
|
<span>Key or folder</span>
|
||||||
|
<input autofocus="autofocus" type="text" value={{left-trim api.data.Key parent.Key}} name="additional" oninput={{action api.change}} placeholder="Key or folder" />
|
||||||
|
<em>To create a folder, end a key with <code>/</code></em>
|
||||||
|
</label>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (or (eq (left-trim api.data.Key parent.Key) '') (not-eq (last api.data.Key) '/'))}}
|
||||||
|
<div>
|
||||||
|
<div class="type-toggle">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="json" checked={{if json 'checked'}} onchange={{action api.change}} />
|
||||||
|
<span>Code</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label for="" class="type-text{{if api.data.error.Value ' has-error'}}">
|
||||||
|
<span>Value</span>
|
||||||
|
{{#if json}}
|
||||||
|
<CodeEditor @value={{atob api.data.Value}} @onkeyup={{action api.change "value"}} />
|
||||||
|
{{else}}
|
||||||
|
<textarea autofocus={{not api.isCreate}} name="value" oninput={{action api.change}}>{{atob api.data.Value}}</textarea>
|
||||||
|
{{/if}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</fieldset>
|
||||||
|
{{#if api.isCreate}}
|
||||||
|
<button type="submit" disabled={{or api.data.isPristine api.data.isInvalid api.disabled}}>Save</button>
|
||||||
|
<button type="reset" onclick={{action oncancel api.data}} disabled={{api.disabled}}>Cancel</button>
|
||||||
|
{{else}}
|
||||||
|
<button type="submit" disabled={{or api.data.isInvalid api.disabled}}>Save</button>
|
||||||
|
<button type="reset" onclick={{action oncancel api.data}} disabled={{api.disabled}}>Cancel</button>
|
||||||
|
<ConfirmationDialog @message="Are you sure you want to delete this key?">
|
||||||
|
<BlockSlot @name="action" as |confirm|>
|
||||||
|
<button data-test-delete type="button" class="type-delete" {{action confirm api.delete}} disabled={{api.disabled}}>Delete</button>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="dialog" as |execute cancel message|>
|
||||||
|
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
|
||||||
|
</BlockSlot>
|
||||||
|
</ConfirmationDialog>
|
||||||
|
{{/if}}
|
||||||
|
</form>
|
||||||
|
</BlockSlot>
|
||||||
|
</DataForm>
|
|
@ -1,45 +1,33 @@
|
||||||
import Controller from '@ember/controller';
|
import Component from '@ember/component';
|
||||||
import { get, set } from '@ember/object';
|
import { get, set } from '@ember/object';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Component.extend({
|
||||||
dom: service('dom'),
|
tagName: '',
|
||||||
builder: service('form'),
|
|
||||||
encoder: service('btoa'),
|
encoder: service('btoa'),
|
||||||
json: true,
|
json: true,
|
||||||
init: function() {
|
ondelete: function() {
|
||||||
this._super(...arguments);
|
this.onsubmit(...arguments);
|
||||||
this.form = this.builder.form('kv');
|
|
||||||
},
|
},
|
||||||
setProperties: function(model) {
|
oncancel: function() {
|
||||||
// essentially this replaces the data with changesets
|
this.onsubmit(...arguments);
|
||||||
this._super(
|
|
||||||
Object.keys(model).reduce((prev, key, i) => {
|
|
||||||
switch (key) {
|
|
||||||
case 'item':
|
|
||||||
prev[key] = this.form.setData(prev[key]).getData();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return prev;
|
|
||||||
}, model)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
onsubmit: function() {},
|
||||||
actions: {
|
actions: {
|
||||||
change: function(e, value, item) {
|
change: function(e, form) {
|
||||||
const event = this.dom.normalizeEvent(e, value);
|
const item = form.getData();
|
||||||
const form = this.form;
|
|
||||||
try {
|
try {
|
||||||
form.handleEvent(event);
|
form.handleEvent(e);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const target = event.target;
|
const target = e.target;
|
||||||
let parent;
|
let parent;
|
||||||
switch (target.name) {
|
switch (target.name) {
|
||||||
case 'value':
|
case 'value':
|
||||||
set(this.item, 'Value', this.encoder.execute(target.value));
|
set(item, 'Value', this.encoder.execute(target.value));
|
||||||
break;
|
break;
|
||||||
case 'additional':
|
case 'additional':
|
||||||
parent = get(this, 'parent.Key');
|
parent = get(this, 'parent.Key');
|
||||||
set(this.item, 'Key', `${parent !== '/' ? parent : ''}${target.value}`);
|
set(item, 'Key', `${parent !== '/' ? parent : ''}${target.value}`);
|
||||||
break;
|
break;
|
||||||
case 'json':
|
case 'json':
|
||||||
// TODO: Potentially save whether json has been clicked to the model,
|
// TODO: Potentially save whether json has been clicked to the model,
|
|
@ -0,0 +1,58 @@
|
||||||
|
<DataWriter
|
||||||
|
@sink={{concat '/' dc '/' nspace '/kv/'}}
|
||||||
|
@type="kv"
|
||||||
|
@label="key"
|
||||||
|
@ondelete={{action ondelete}}
|
||||||
|
as |writer|>
|
||||||
|
<BlockSlot @name="content">
|
||||||
|
{{#if (gt items.length 0)}}
|
||||||
|
<TabularCollection class="consul-kv-list" @items={{items}} as |item index|>
|
||||||
|
<BlockSlot @name="header">
|
||||||
|
<th>Name</th>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="row">
|
||||||
|
<td data-test-kv={{item.Key}} class={{if item.isFolder 'folder' 'file' }}>
|
||||||
|
<a href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key}}>{{right-trim (left-trim item.Key parent.Key) '/'}}</a>
|
||||||
|
</td>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="actions" as |index change checked|>
|
||||||
|
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
|
||||||
|
<BlockSlot @name="trigger">
|
||||||
|
More
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="menu" as |confirm send keypressClick|>
|
||||||
|
<li role="none">
|
||||||
|
<a data-test-edit role="menuitem" tabindex="-1" href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key}}>{{if item.isFolder 'View' 'Edit'}}</a>
|
||||||
|
</li>
|
||||||
|
<li role="none" class="dangerous">
|
||||||
|
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
|
||||||
|
<div role="menu">
|
||||||
|
<div class="confirmation-alert warning">
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
Confirm Delete
|
||||||
|
</header>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete this key?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li class="dangerous">
|
||||||
|
<button tabindex="-1" type="button" class="type-delete" onclick={{queue (action change) (action writer.delete item)}}>Delete</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label for={{confirm}}>Cancel</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</BlockSlot>
|
||||||
|
</PopoverMenu>
|
||||||
|
</BlockSlot>
|
||||||
|
</TabularCollection>
|
||||||
|
{{else}}
|
||||||
|
{{yield}}
|
||||||
|
{{/if}}
|
||||||
|
</BlockSlot>
|
||||||
|
</DataWriter>
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
ondelete: function() {},
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default (collection, clickable, attribute, deletable) => () => {
|
||||||
|
return collection('.consul-kv-list [data-test-tabular-row]', {
|
||||||
|
name: attribute('data-test-kv', '[data-test-kv]'),
|
||||||
|
kv: clickable('a'),
|
||||||
|
actions: clickable('label'),
|
||||||
|
...deletable(),
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,54 @@
|
||||||
|
<DataForm
|
||||||
|
@dc={{dc}}
|
||||||
|
@nspace={{nspace}}
|
||||||
|
@item={{item}}
|
||||||
|
@type="session"
|
||||||
|
@onsubmit={{action onsubmit}}
|
||||||
|
as |api|
|
||||||
|
>
|
||||||
|
<BlockSlot @name="form">
|
||||||
|
<div class="definition-table" data-test-session={{api.data.ID}}>
|
||||||
|
<h2>
|
||||||
|
<a href="{{env 'CONSUL_DOCS_URL'}}/internals/sessions.html#session-design" rel="help noopener noreferrer" target="_blank">Lock Session</a>
|
||||||
|
</h2>
|
||||||
|
<dl>
|
||||||
|
<dt>Name</dt>
|
||||||
|
<dd>{{api.data.Name}}</dd>
|
||||||
|
<dt>Agent</dt>
|
||||||
|
<dd>
|
||||||
|
<a href={{href-to 'dc.nodes.show' api.data.Node}}>{{api.data.Node}}</a>
|
||||||
|
</dd>
|
||||||
|
<dt>ID</dt>
|
||||||
|
<dd>{{api.data.ID}}</dd>
|
||||||
|
<dt>Behavior</dt>
|
||||||
|
<dd>{{api.data.Behavior}}</dd>
|
||||||
|
{{#if form.data.Delay }}
|
||||||
|
<dt>Delay</dt>
|
||||||
|
<dd>{{api.data.LockDelay}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
{{#if form.data.TTL }}
|
||||||
|
<dt>TTL</dt>
|
||||||
|
<dd>{{api.data.TTL}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (gt api.data.Checks.length 0)}}
|
||||||
|
<dt>Health Checks</dt>
|
||||||
|
<dd>
|
||||||
|
{{ join ', ' api.data.Checks}}
|
||||||
|
</dd>
|
||||||
|
{{/if}}
|
||||||
|
</dl>
|
||||||
|
<ConfirmationDialog @message="Are you sure you want to invalidate this session?">
|
||||||
|
<BlockSlot @name="action" as |confirm|>
|
||||||
|
<button type="button" data-test-delete class="type-delete" {{action confirm api.delete session}} disabled={{api.disabled}}>Invalidate Session</button>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="dialog" as |execute cancel message|>
|
||||||
|
<p>
|
||||||
|
{{message}}
|
||||||
|
</p>
|
||||||
|
<button type="button" class="type-delete" {{action execute}}>Confirm Invalidation</button>
|
||||||
|
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
|
||||||
|
</BlockSlot>
|
||||||
|
</ConfirmationDialog>
|
||||||
|
</div>
|
||||||
|
</BlockSlot>
|
||||||
|
</DataForm>
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({});
|
|
@ -4,6 +4,7 @@
|
||||||
<DataWriter
|
<DataWriter
|
||||||
@sink={{concat '/' nspace '/' (or data.Datacenter dc) '/' type '/'}}
|
@sink={{concat '/' nspace '/' (or data.Datacenter dc) '/' type '/'}}
|
||||||
@type={{type}}
|
@type={{type}}
|
||||||
|
@label={{label}}
|
||||||
@ondelete={{action ondelete}}
|
@ondelete={{action ondelete}}
|
||||||
@onchange={{action onsubmit}}
|
@onchange={{action onsubmit}}
|
||||||
as |writer|>
|
as |writer|>
|
||||||
|
@ -19,12 +20,13 @@
|
||||||
) as |api|}}
|
) as |api|}}
|
||||||
|
|
||||||
{{yield api}}
|
{{yield api}}
|
||||||
|
{{#if hasError}}
|
||||||
<BlockSlot @name="error">
|
<BlockSlot @name="error">
|
||||||
<YieldSlot @name="error">
|
<YieldSlot @name="error">
|
||||||
{{yield api}}
|
{{yield api}}
|
||||||
</YieldSlot>
|
</YieldSlot>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<BlockSlot @name="content">
|
<BlockSlot @name="content">
|
||||||
<YieldSlot @name="form">
|
<YieldSlot @name="form">
|
||||||
|
|
|
@ -27,6 +27,10 @@ export default Component.extend(Slotted, {
|
||||||
// this lets us load view only data that doesn't have a form
|
// this lets us load view only data that doesn't have a form
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
willRender: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
set(this, 'hasError', this._isRegistered('error'));
|
||||||
|
},
|
||||||
willDestroyElement: function() {
|
willDestroyElement: function() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
if (get(this, 'data.isNew')) {
|
if (get(this, 'data.isNew')) {
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<Notification @after={{queue (action dispatch "RESET") (action ondelete)}}>
|
<Notification @after={{queue (action dispatch "RESET") (action ondelete)}}>
|
||||||
<p data-notification role="alert" class="success notification-delete">
|
<p data-notification role="alert" class="success notification-delete">
|
||||||
<strong>Success!</strong>
|
<strong>Success!</strong>
|
||||||
Your {{type}} has been deleted.
|
Your {{or label type}} has been deleted.
|
||||||
</p>
|
</p>
|
||||||
</Notification>
|
</Notification>
|
||||||
{{/yield-slot}}
|
{{/yield-slot}}
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<p data-notification role="alert" class="success notification-update">
|
<p data-notification role="alert" class="success notification-update">
|
||||||
<strong>Success!</strong>
|
<strong>Success!</strong>
|
||||||
Your {{type}} has been saved.
|
Your {{or label type}} has been saved.
|
||||||
</p>
|
</p>
|
||||||
{{/yield-slot}}
|
{{/yield-slot}}
|
||||||
</Notification>
|
</Notification>
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
<Notification @after={{action dispatch "RESET"}}>
|
<Notification @after={{action dispatch "RESET"}}>
|
||||||
<p data-notification role="alert" class="error notification-update">
|
<p data-notification role="alert" class="error notification-update">
|
||||||
<strong>Error!</strong>
|
<strong>Error!</strong>
|
||||||
There was an error saving your {{type}}.
|
There was an error saving your {{or label type}}.
|
||||||
{{#if (and api.error.status api.error.detail)}}
|
{{#if (and api.error.status api.error.detail)}}
|
||||||
<br />{{api.error.status}}: {{api.error.detail}}
|
<br />{{api.error.status}}: {{api.error.detail}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -24,9 +24,15 @@ export default Component.extend({
|
||||||
$el.remove();
|
$el.remove();
|
||||||
this.notify.clearMessages();
|
this.notify.clearMessages();
|
||||||
if (typeof this.after === 'function') {
|
if (typeof this.after === 'function') {
|
||||||
Promise.resolve(this.after()).then(res => {
|
Promise.resolve(this.after())
|
||||||
this.notify.add(options);
|
.catch(e => {
|
||||||
});
|
if (e.name !== 'TransitionAborted') {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
this.notify.add(options);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.notify.add(options);
|
this.notify.add(options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
import Controller from './edit';
|
|
||||||
export default Controller.extend();
|
|
|
@ -1,2 +0,0 @@
|
||||||
import Controller from './index';
|
|
||||||
export default Controller.extend();
|
|
|
@ -1,2 +0,0 @@
|
||||||
import Controller from './create';
|
|
||||||
export default Controller.extend();
|
|
|
@ -1,35 +0,0 @@
|
||||||
import Mixin from '@ember/object/mixin';
|
|
||||||
import { get, set } from '@ember/object';
|
|
||||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
|
||||||
|
|
||||||
export default Mixin.create(WithBlockingActions, {
|
|
||||||
// afterCreate just calls afterUpdate
|
|
||||||
afterUpdate: function(item, parent) {
|
|
||||||
const key = get(parent, 'Key');
|
|
||||||
if (key === '/') {
|
|
||||||
return this.transitionTo('dc.kv.index');
|
|
||||||
} else {
|
|
||||||
return this.transitionTo('dc.kv.folder', key);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
afterDelete: function(item, parent) {
|
|
||||||
if (this.routeName === 'dc.kv.folder') {
|
|
||||||
return this.refresh();
|
|
||||||
}
|
|
||||||
return this._super(...arguments);
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
invalidateSession: function(item) {
|
|
||||||
const controller = this.controller;
|
|
||||||
const repo = this.sessionRepo;
|
|
||||||
return this.feedback.execute(() => {
|
|
||||||
return repo.remove(item).then(() => {
|
|
||||||
const item = get(controller, 'item');
|
|
||||||
set(item, 'Session', null);
|
|
||||||
delete item.Session;
|
|
||||||
set(controller, 'session', null);
|
|
||||||
});
|
|
||||||
}, 'deletesession');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,36 +1,5 @@
|
||||||
import Route from '@ember/routing/route';
|
import Route from './edit';
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { hash } from 'rsvp';
|
|
||||||
import { get } from '@ember/object';
|
|
||||||
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
|
|
||||||
|
|
||||||
export default Route.extend(WithKvActions, {
|
export default Route.extend({
|
||||||
templateName: 'dc/kv/edit',
|
templateName: 'dc/kv/edit',
|
||||||
repo: service('repository/kv'),
|
|
||||||
beforeModel: function() {
|
|
||||||
this.repo.invalidate();
|
|
||||||
},
|
|
||||||
model: function(params) {
|
|
||||||
const key = params.key || '/';
|
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
|
||||||
this.item = this.repo.create({
|
|
||||||
Datacenter: dc,
|
|
||||||
Namespace: nspace,
|
|
||||||
});
|
|
||||||
return hash({
|
|
||||||
create: true,
|
|
||||||
isLoading: false,
|
|
||||||
item: this.item,
|
|
||||||
parent: this.repo.findBySlug(key, dc, nspace),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
setupController: function(controller, model) {
|
|
||||||
controller.setProperties(model);
|
|
||||||
},
|
|
||||||
deactivate: function() {
|
|
||||||
if (get(this.item, 'isNew')) {
|
|
||||||
this.item.destroyRecord();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,11 +2,10 @@ import Route from '@ember/routing/route';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { hash } from 'rsvp';
|
import { hash } from 'rsvp';
|
||||||
import { get } from '@ember/object';
|
import { get } from '@ember/object';
|
||||||
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
|
|
||||||
|
|
||||||
import ascend from 'consul-ui/utils/ascend';
|
import ascend from 'consul-ui/utils/ascend';
|
||||||
|
|
||||||
export default Route.extend(WithKvActions, {
|
export default Route.extend({
|
||||||
repo: service('repository/kv'),
|
repo: service('repository/kv'),
|
||||||
sessionRepo: service('repository/session'),
|
sessionRepo: service('repository/session'),
|
||||||
model: function(params) {
|
model: function(params) {
|
||||||
|
@ -14,20 +13,26 @@ export default Route.extend(WithKvActions, {
|
||||||
const dc = this.modelFor('dc').dc.Name;
|
const dc = this.modelFor('dc').dc.Name;
|
||||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||||
return hash({
|
return hash({
|
||||||
isLoading: false,
|
dc: dc,
|
||||||
parent: this.repo.findBySlug(ascend(key, 1) || '/', dc, nspace),
|
nspace: nspace || 'default',
|
||||||
item: this.repo.findBySlug(key, dc, nspace),
|
parent:
|
||||||
|
typeof key !== 'undefined'
|
||||||
|
? this.repo.findBySlug(ascend(key, 1) || '/', dc, nspace)
|
||||||
|
: this.repo.findBySlug('/', dc, nspace),
|
||||||
|
item: typeof key !== 'undefined' ? this.repo.findBySlug(key, dc, nspace) : undefined,
|
||||||
session: null,
|
session: null,
|
||||||
}).then(model => {
|
}).then(model => {
|
||||||
// TODO: Consider loading this after initial page load
|
// TODO: Consider loading this after initial page load
|
||||||
const session = get(model.item, 'Session');
|
if (typeof model.item !== 'undefined') {
|
||||||
if (session) {
|
const session = get(model.item, 'Session');
|
||||||
return hash({
|
if (session) {
|
||||||
...model,
|
return hash({
|
||||||
...{
|
...model,
|
||||||
session: this.sessionRepo.findByKey(session, dc, nspace),
|
...{
|
||||||
},
|
session: this.sessionRepo.findByKey(session, dc, nspace),
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return model;
|
return model;
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { inject as service } from '@ember/service';
|
||||||
import { hash } from 'rsvp';
|
import { hash } from 'rsvp';
|
||||||
import { get } from '@ember/object';
|
import { get } from '@ember/object';
|
||||||
import isFolder from 'consul-ui/utils/isFolder';
|
import isFolder from 'consul-ui/utils/isFolder';
|
||||||
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
|
|
||||||
|
|
||||||
export default Route.extend(WithKvActions, {
|
export default Route.extend({
|
||||||
queryParams: {
|
queryParams: {
|
||||||
search: {
|
search: {
|
||||||
as: 'filter',
|
as: 'filter',
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { setProperties } from '@ember/object';
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
settings: service('settings'),
|
settings: service('settings'),
|
||||||
intention: service('repository/intention'),
|
intention: service('repository/intention'),
|
||||||
|
kv: service('repository/kv'),
|
||||||
session: service('repository/session'),
|
session: service('repository/session'),
|
||||||
prepare: function(sink, data, instance) {
|
prepare: function(sink, data, instance) {
|
||||||
return setProperties(instance, data);
|
return setProperties(instance, data);
|
||||||
|
|
|
@ -113,6 +113,7 @@ export default Service.extend({
|
||||||
repo.findInstanceBySlug(rest[0], rest[1], rest[2], dc, nspace, configuration);
|
repo.findInstanceBySlug(rest[0], rest[1], rest[2], dc, nspace, configuration);
|
||||||
break;
|
break;
|
||||||
case 'policy':
|
case 'policy':
|
||||||
|
case 'kv':
|
||||||
case 'intention':
|
case 'intention':
|
||||||
slug = rest[0];
|
slug = rest[0];
|
||||||
if (slug) {
|
if (slug) {
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
<form>
|
|
||||||
<fieldset>
|
|
||||||
{{#if create }}
|
|
||||||
<label class="type-text{{if item.error.Key ' has-error'}}">
|
|
||||||
<span>Key or folder</span>
|
|
||||||
<input autofocus="autofocus" type="text" value={{left-trim item.Key parent.Key}} name="additional" oninput={{action 'change'}} placeholder="Key or folder" />
|
|
||||||
<em>To create a folder, end a key with <code>/</code></em>
|
|
||||||
</label>
|
|
||||||
{{/if}}
|
|
||||||
{{#if (or (eq (left-trim item.Key parent.Key) '') (not-eq (last item.Key) '/')) }}
|
|
||||||
<div>
|
|
||||||
<div class="type-toggle">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" name="json" checked={{if json 'checked' }} onchange={{action 'change'}} />
|
|
||||||
<span>Code</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label for="" class="type-text{{if item.error.Value ' has-error'}}">
|
|
||||||
<span>Value</span>
|
|
||||||
{{#if json}}
|
|
||||||
<CodeEditor @value={{atob item.Value}} @onkeyup={{action "change" "value"}} />
|
|
||||||
{{else}}
|
|
||||||
<textarea autofocus={{not create}} name="value" oninput={{action 'change'}}>{{atob item.Value}}</textarea>
|
|
||||||
{{/if}}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</fieldset>
|
|
||||||
{{!TODO This has a <div> around it in acls, remove or add for consistency }}
|
|
||||||
{{#if create }}
|
|
||||||
{{! we only need to check for an empty keyname here as ember munges autofocus, once we have autofocus back revisit this}}
|
|
||||||
<button type="submit" {{ action "create" item parent}} disabled={{if (or item.isPristine item.isInvalid (eq (left-trim item.Key parent.Key) '')) 'disabled'}}>Save</button>
|
|
||||||
{{ else }}
|
|
||||||
<button type="submit" {{ action "update" item parent}} disabled={{if item.isInvalid 'disabled'}}>Save</button>
|
|
||||||
<button type="reset" {{ action "cancel" item parent}}>Cancel changes</button>
|
|
||||||
<ConfirmationDialog @message="Are you sure you want to delete this key?">
|
|
||||||
<BlockSlot @name="action" as |confirm|>
|
|
||||||
<button data-test-delete type="button" class="type-delete" {{action confirm 'delete' item parent}}>Delete</button>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="dialog" as |execute cancel message|>
|
|
||||||
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
|
|
||||||
</BlockSlot>
|
|
||||||
</ConfirmationDialog>
|
|
||||||
{{/if}}
|
|
||||||
</form>
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
{{#if (eq type 'create')}}
|
|
||||||
{{#if (eq status 'success') }}
|
|
||||||
Your key has been added.
|
|
||||||
{{else}}
|
|
||||||
There was an error adding your key.
|
|
||||||
{{/if}}
|
|
||||||
{{else if (eq type 'update') }}
|
|
||||||
{{#if (eq status 'success') }}
|
|
||||||
Your key has been saved.
|
|
||||||
{{else}}
|
|
||||||
There was an error saving your key.
|
|
||||||
{{/if}}
|
|
||||||
{{ else if (eq type 'delete')}}
|
|
||||||
{{#if (eq status 'success') }}
|
|
||||||
Your key was deleted.
|
|
||||||
{{else}}
|
|
||||||
There was an error deleting your key.
|
|
||||||
{{/if}}
|
|
||||||
{{ else if (eq type 'deletesession')}}
|
|
||||||
{{#if (eq status 'success') }}
|
|
||||||
Your session was invalidated.
|
|
||||||
{{else}}
|
|
||||||
There was an error invalidating your session.
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{#let error.errors.firstObject as |error|}}
|
|
||||||
{{#if error.detail }}
|
|
||||||
<br />{{concat '(' (if error.status (concat error.status ': ')) error.detail ')'}}
|
|
||||||
{{/if}}
|
|
||||||
{{/let}}
|
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
{{#if create }}
|
{{#if item.Key }}
|
||||||
{{title 'New Key/Value'}}
|
|
||||||
{{else}}
|
|
||||||
{{title 'Edit Key/Value'}}
|
{{title 'Edit Key/Value'}}
|
||||||
|
{{else}}
|
||||||
|
{{title 'New Key/Value'}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<AppView @class="kv edit" @loading={{isLoading}}>
|
<AppView @class="kv edit">
|
||||||
<BlockSlot @name="notification" as |status type item error|>
|
|
||||||
{{partial 'dc/kv/notifications'}}
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="breadcrumbs">
|
<BlockSlot @name="breadcrumbs">
|
||||||
<ol>
|
<ol>
|
||||||
<li><a data-test-back href={{href-to 'dc.kv.index'}}>Key / Values</a></li>
|
<li><a data-test-back href={{href-to 'dc.kv.index'}}>Key / Values</a></li>
|
||||||
|
@ -27,56 +24,25 @@
|
||||||
</h1>
|
</h1>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name="content">
|
<BlockSlot @name="content">
|
||||||
{{#if session}}
|
{{#if session}}
|
||||||
<p class="notice warning">
|
<p class="notice warning">
|
||||||
<strong>Warning.</strong> This KV has a lock session. You can edit KV's with lock sessions, but we recommend doing so with care, or not doing so at all. It may negatively impact the active node it's associated with. See below for more details on the Lock Session and see <a href="{{env 'CONSUL_DOCS_URL'}}/internals/sessions.html" target="_blank" rel="noopener noreferrer">our documentation</a> for more information.
|
<strong>Warning.</strong> This KV has a lock session. You can edit KV's with lock sessions, but we recommend doing so with care, or not doing so at all. It may negatively impact the active node it's associated with. See below for more details on the Lock Session and see <a href="{{env 'CONSUL_DOCS_URL'}}/internals/sessions.html" target="_blank" rel="noopener noreferrer">our documentation</a> for more information.
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{partial 'dc/kv/form'}}
|
<ConsulKvForm
|
||||||
{{#if session}}
|
@item={{item}}
|
||||||
<div class="definition-table" data-test-session={{session.ID}}>
|
@dc={{dc}}
|
||||||
<h2>
|
@nspace={{nspace}}
|
||||||
<a href="{{env 'CONSUL_DOCS_URL'}}/internals/sessions.html#session-design" rel="help noopener noreferrer" target="_blank">Lock Session</a>
|
@onsubmit={{if (eq parent.Key '/') (transition-to 'dc.kv.index') (transition-to 'dc.kv.folder' parent.Key)}}
|
||||||
</h2>
|
@parent={{parent}}
|
||||||
<dl>
|
/>
|
||||||
<dt>Name</dt>
|
{{#if session}}
|
||||||
<dd>{{session.Name}}</dd>
|
<ConsulSessionForm
|
||||||
<dt>Agent</dt>
|
@item={{session}}
|
||||||
<dd>
|
@dc={{dc}}
|
||||||
<a href={{href-to 'dc.nodes.show' session.Node}}>{{session.Node}}</a>
|
@nspace={{nspace}}
|
||||||
</dd>
|
@onsubmit={{action (mut session) undefined}}
|
||||||
<dt>ID</dt>
|
/>
|
||||||
<dd>{{session.ID}}</dd>
|
{{/if}}
|
||||||
<dt>Behavior</dt>
|
|
||||||
<dd>{{session.Behavior}}</dd>
|
|
||||||
{{#if session.Delay }}
|
|
||||||
<dt>Delay</dt>
|
|
||||||
<dd>{{session.LockDelay}}</dd>
|
|
||||||
{{/if}}
|
|
||||||
{{#if session.TTL }}
|
|
||||||
<dt>TTL</dt>
|
|
||||||
<dd>{{session.TTL}}</dd>
|
|
||||||
{{/if}}
|
|
||||||
{{#if (gt session.Checks.length 0)}}
|
|
||||||
<dt>Health Checks</dt>
|
|
||||||
<dd>
|
|
||||||
{{ join ', ' session.Checks}}
|
|
||||||
</dd>
|
|
||||||
{{/if}}
|
|
||||||
</dl>
|
|
||||||
<ConfirmationDialog @message="Are you sure you want to invalidate this session?">
|
|
||||||
<BlockSlot @name="action" as |confirm|>
|
|
||||||
<button type="button" data-test-delete class="type-delete" {{action confirm "invalidateSession" session}}>Invalidate Session</button>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="dialog" as |execute cancel message|>
|
|
||||||
<p>
|
|
||||||
{{message}}
|
|
||||||
</p>
|
|
||||||
<button type="button" class="type-delete" {{action execute}}>Confirm Invalidation</button>
|
|
||||||
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
|
|
||||||
</BlockSlot>
|
|
||||||
</ConfirmationDialog>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</AppView>
|
</AppView>
|
|
@ -1,8 +1,5 @@
|
||||||
{{title 'Key/Value'}}
|
{{title 'Key/Value'}}
|
||||||
<AppView @class="kv list" @loading={{isLoading}}>
|
<AppView @class="kv list">
|
||||||
<BlockSlot @name="notification" as |status type|>
|
|
||||||
{{partial 'dc/kv/notifications'}}
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="breadcrumbs">
|
<BlockSlot @name="breadcrumbs">
|
||||||
<ol>
|
<ol>
|
||||||
{{#if (not-eq parent.Key '/') }}
|
{{#if (not-eq parent.Key '/') }}
|
||||||
|
@ -41,82 +38,41 @@
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name="content">
|
<BlockSlot @name="content">
|
||||||
<ChangeableSet @dispatcher={{searchable 'kv' items}} @terms={{search}}>
|
<ChangeableSet @dispatcher={{searchable 'kv' items}} @terms={{search}}>
|
||||||
<BlockSlot @name="set" as |filtered|>
|
<BlockSlot @name="content" as |filtered|>
|
||||||
<TabularCollection @items={{sort-by "isFolder:desc" "Key:asc" filtered}} as |item index|>
|
<ConsulKvList
|
||||||
<BlockSlot @name="header">
|
@items={{sort-by "isFolder:desc" "Key:asc" filtered}}
|
||||||
<th>Name</th>
|
@parent={{parent}}
|
||||||
</BlockSlot>
|
@ondelete={{refresh-route}}
|
||||||
<BlockSlot @name="row">
|
>
|
||||||
<td data-test-kv={{item.Key}} class={{if item.isFolder 'folder' 'file' }}>
|
<EmptyState @allowLogin={{true}}>
|
||||||
<a href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key}}>{{right-trim (left-trim item.Key parent.Key) '/'}}</a>
|
<BlockSlot @name="header">
|
||||||
</td>
|
<h2>
|
||||||
</BlockSlot>
|
{{#if (gt items.length 0)}}
|
||||||
<BlockSlot @name="actions" as |index change checked|>
|
No K/V pairs found
|
||||||
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
|
{{else}}
|
||||||
<BlockSlot @name="trigger">
|
Welcome to Key/Value
|
||||||
More
|
{{/if}}
|
||||||
</BlockSlot>
|
</h2>
|
||||||
<BlockSlot @name="menu" as |confirm send keypressClick clickTrigger|>
|
</BlockSlot>
|
||||||
<li role="none">
|
<BlockSlot @name="body">
|
||||||
<a data-test-edit role="menuitem" tabindex="-1" href={{href-to (if item.isFolder 'dc.kv.folder' 'dc.kv.edit') item.Key}}>{{if item.isFolder 'View' 'Edit'}}</a>
|
<p>
|
||||||
</li>
|
{{#if (gt items.length 0)}}
|
||||||
<li role="none" class="dangerous">
|
No K/V pairs where found matching that search, or you may not have access to view the K/V pairs you are searching for.
|
||||||
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
|
{{else}}
|
||||||
<div role="menu">
|
You don't have any K/V pairs, or you may not have access to view K/V pairs yet.
|
||||||
<div class="confirmation-alert warning">
|
{{/if}}
|
||||||
<div>
|
</p>
|
||||||
<header>
|
</BlockSlot>
|
||||||
Confirm Delete
|
<BlockSlot @name="actions">
|
||||||
</header>
|
<li class="docs-link">
|
||||||
<p>
|
<a href="{{env 'CONSUL_DOCS_URL'}}/agent/kv" rel="noopener noreferrer" target="_blank">Documentation on K/V</a>
|
||||||
Are you sure you want to delete this key?
|
</li>
|
||||||
</p>
|
<li class="learn-link">
|
||||||
</div>
|
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/getting-started/kv" rel="noopener noreferrer" target="_blank">Read the guide</a>
|
||||||
<ul>
|
</li>
|
||||||
<li class="dangerous">
|
</BlockSlot>
|
||||||
<button tabindex="-1" type="button" class="type-delete" onclick={{queue (action send 'delete' item) (action clickTrigger)}}>Delete</button>
|
</EmptyState>
|
||||||
</li>
|
</ConsulKvList>
|
||||||
<li>
|
|
||||||
<label for={{confirm}}>Cancel</label>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</BlockSlot>
|
|
||||||
</PopoverMenu>
|
|
||||||
</BlockSlot>
|
|
||||||
</TabularCollection>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="empty">
|
|
||||||
<EmptyState @allowLogin={{true}}>
|
|
||||||
<BlockSlot @name="header">
|
|
||||||
<h2>
|
|
||||||
{{#if (gt items.length 0)}}
|
|
||||||
No K/V pairs found
|
|
||||||
{{else}}
|
|
||||||
Welcome to Key/Value
|
|
||||||
{{/if}}
|
|
||||||
</h2>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="body">
|
|
||||||
<p>
|
|
||||||
{{#if (gt items.length 0)}}
|
|
||||||
No K/V pairs where found matching that search, or you may not have access to view the K/V pairs you are searching for.
|
|
||||||
{{else}}
|
|
||||||
You don't have any K/V pairs, or you may not have access to view K/V pairs yet.
|
|
||||||
{{/if}}
|
|
||||||
</p>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="actions">
|
|
||||||
<li class="docs-link">
|
|
||||||
<a href="{{env 'CONSUL_DOCS_URL'}}/agent/kv" rel="noopener noreferrer" target="_blank">Documentation on K/V</a>
|
|
||||||
</li>
|
|
||||||
<li class="learn-link">
|
|
||||||
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/getting-started/kv" rel="noopener noreferrer" target="_blank">Read the guide</a>
|
|
||||||
</li>
|
|
||||||
</BlockSlot>
|
|
||||||
</EmptyState>
|
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</ChangeableSet>
|
</ChangeableSet>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
@setupApplicationTest
|
@setupApplicationTest
|
||||||
Feature: components / kv-filter
|
Feature: components / kv-filter
|
||||||
Scenario: Filtering using the freetext filter
|
Scenario: Filtering using the freetext filter with [Text]
|
||||||
Given 1 datacenter model with the value "dc-1"
|
Given 1 datacenter model with the value "dc-1"
|
||||||
And 2 [Model] models from yaml
|
And 2 [Model] models from yaml
|
||||||
---
|
---
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
@setupApplicationTest
|
||||||
|
Feature: dc / kvs / deleting: Deleting items with confirmations, success and error notifications
|
||||||
|
Background:
|
||||||
|
Given 1 datacenter model with the value "datacenter"
|
||||||
|
Scenario: Deleting a kv model from the kv listing page
|
||||||
|
Given 1 kv model from yaml
|
||||||
|
---
|
||||||
|
["key-name"]
|
||||||
|
---
|
||||||
|
When I visit the kvs page for yaml
|
||||||
|
---
|
||||||
|
dc: datacenter
|
||||||
|
---
|
||||||
|
And I click actions on the kvs
|
||||||
|
And I click delete on the kvs
|
||||||
|
And I click confirmDelete on the kvs
|
||||||
|
Then a DELETE request was made to "/v1/kv/key-name?dc=datacenter&ns=@!namespace"
|
||||||
|
And "[data-notification]" has the "notification-delete" class
|
||||||
|
And "[data-notification]" has the "success" class
|
||||||
|
Scenario: Deleting an kv from the kv detail page
|
||||||
|
When I visit the kv page for yaml
|
||||||
|
---
|
||||||
|
dc: datacenter
|
||||||
|
kv: key-name
|
||||||
|
---
|
||||||
|
And I click delete
|
||||||
|
And I click confirmDelete
|
||||||
|
Then a DELETE request was made to "/v1/kv/key-name?dc=datacenter&ns=@!namespace"
|
||||||
|
And "[data-notification]" has the "notification-delete" class
|
||||||
|
And "[data-notification]" has the "success" class
|
||||||
|
Scenario: Deleting an kv from the kv detail page and getting an error
|
||||||
|
When I visit the kv page for yaml
|
||||||
|
---
|
||||||
|
dc: datacenter
|
||||||
|
kv: key-name
|
||||||
|
---
|
||||||
|
Given the url "/v1/kv/key-name?dc=datacenter&ns=@!namespace" responds with a 500 status
|
||||||
|
And I click delete
|
||||||
|
And I click confirmDelete
|
||||||
|
And "[data-notification]" has the "notification-update" class
|
||||||
|
And "[data-notification]" has the "error" class
|
||||||
|
|
|
@ -21,12 +21,12 @@ Feature: dc / kvs / sessions / invalidate: Invalidate Lock Sessions
|
||||||
And I click confirmDelete on the session
|
And I click confirmDelete on the session
|
||||||
Then a PUT request was made to "/v1/session/destroy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter&ns=@!namespace"
|
Then a PUT request was made to "/v1/session/destroy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter&ns=@!namespace"
|
||||||
Then the url should be /datacenter/kv/key/edit
|
Then the url should be /datacenter/kv/key/edit
|
||||||
And "[data-notification]" has the "notification-deletesession" class
|
And "[data-notification]" has the "notification-delete" class
|
||||||
And "[data-notification]" has the "success" class
|
And "[data-notification]" has the "success" class
|
||||||
Scenario: Invalidating a lock session and receiving an error
|
Scenario: Invalidating a lock session and receiving an error
|
||||||
Given the url "/v1/session/destroy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter&ns=@!namespace" responds with a 500 status
|
Given the url "/v1/session/destroy/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter&ns=@!namespace" responds with a 500 status
|
||||||
And I click delete on the session
|
And I click delete on the session
|
||||||
And I click confirmDelete on the session
|
And I click confirmDelete on the session
|
||||||
Then the url should be /datacenter/kv/key/edit
|
Then the url should be /datacenter/kv/key/edit
|
||||||
And "[data-notification]" has the "notification-deletesession" class
|
And "[data-notification]" has the "notification-update" class
|
||||||
And "[data-notification]" has the "error" class
|
And "[data-notification]" has the "error" class
|
||||||
|
|
|
@ -23,7 +23,6 @@ Feature: deleting: Deleting items with confirmations, success and error notifica
|
||||||
Where:
|
Where:
|
||||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
| Edit | Listing | Method | URL | Data |
|
| Edit | Listing | Method | URL | Data |
|
||||||
| kv | kvs | DELETE | /v1/kv/key-name?dc=datacenter&ns=@!namespace | ["key-name"] |
|
|
||||||
| token | tokens | DELETE | /v1/acl/token/001fda31-194e-4ff1-a5ec-589abf2cafd0?dc=datacenter&ns=@!namespace | {"AccessorID": "001fda31-194e-4ff1-a5ec-589abf2cafd0"} |
|
| token | tokens | DELETE | /v1/acl/token/001fda31-194e-4ff1-a5ec-589abf2cafd0?dc=datacenter&ns=@!namespace | {"AccessorID": "001fda31-194e-4ff1-a5ec-589abf2cafd0"} |
|
||||||
# | acl | acls | PUT | /v1/acl/destroy/something?dc=datacenter | {"Name": "something", "ID": "something"} |
|
# | acl | acls | PUT | /v1/acl/destroy/something?dc=datacenter | {"Name": "something", "ID": "something"} |
|
||||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
@ -51,7 +50,6 @@ Feature: deleting: Deleting items with confirmations, success and error notifica
|
||||||
Where:
|
Where:
|
||||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
| Model | Method | URL | Slug |
|
| Model | Method | URL | Slug |
|
||||||
| kv | DELETE | /v1/kv/key-name?dc=datacenter&ns=@!namespace | kv: key-name |
|
|
||||||
| token | DELETE | /v1/acl/token/001fda31-194e-4ff1-a5ec-589abf2cafd0?dc=datacenter&ns=@!namespace | token: 001fda31-194e-4ff1-a5ec-589abf2cafd0 |
|
| token | DELETE | /v1/acl/token/001fda31-194e-4ff1-a5ec-589abf2cafd0?dc=datacenter&ns=@!namespace | token: 001fda31-194e-4ff1-a5ec-589abf2cafd0 |
|
||||||
# | acl | PUT | /v1/acl/destroy/something?dc=datacenter | acl: something |
|
# | acl | PUT | /v1/acl/destroy/something?dc=datacenter | acl: something |
|
||||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import steps from '../../steps';
|
||||||
|
|
||||||
|
// step definitions that are shared between features should be moved to the
|
||||||
|
// tests/acceptance/steps/steps.js file
|
||||||
|
|
||||||
|
export default function(assert) {
|
||||||
|
return steps(assert).then('I should find a file', function() {
|
||||||
|
assert.ok(true, this.step);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,26 +0,0 @@
|
||||||
import { module, skip } from 'qunit';
|
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
|
||||||
import { render } from '@ember/test-helpers';
|
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
|
||||||
|
|
||||||
module('Integration | Component | consul-kind', function(hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
skip('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`<ConsulKind />`);
|
|
||||||
|
|
||||||
assert.equal(this.element.textContent.trim(), '');
|
|
||||||
|
|
||||||
// Template block usage:
|
|
||||||
await render(hbs`
|
|
||||||
<ConsulKind>
|
|
||||||
template block text
|
|
||||||
</ConsulKind>
|
|
||||||
`);
|
|
||||||
|
|
||||||
assert.equal(this.element.textContent.trim(), 'template block text');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -41,6 +41,7 @@ import consulTokenListFactory from 'consul-ui/components/consul-token-list/pageo
|
||||||
import consulRoleListFactory from 'consul-ui/components/consul-role-list/pageobject';
|
import consulRoleListFactory from 'consul-ui/components/consul-role-list/pageobject';
|
||||||
import consulPolicyListFactory from 'consul-ui/components/consul-policy-list/pageobject';
|
import consulPolicyListFactory from 'consul-ui/components/consul-policy-list/pageobject';
|
||||||
import consulIntentionListFactory from 'consul-ui/components/consul-intention-list/pageobject';
|
import consulIntentionListFactory from 'consul-ui/components/consul-intention-list/pageobject';
|
||||||
|
import consulKvListFactory from 'consul-ui/components/consul-kv-list/pageobject';
|
||||||
|
|
||||||
// pages
|
// pages
|
||||||
import index from 'consul-ui/tests/pages/index';
|
import index from 'consul-ui/tests/pages/index';
|
||||||
|
@ -96,6 +97,7 @@ const morePopoverMenu = morePopoverMenuFactory(clickable);
|
||||||
const popoverSelect = popoverSelectFactory(clickable, collection);
|
const popoverSelect = popoverSelectFactory(clickable, collection);
|
||||||
|
|
||||||
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
|
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
|
||||||
|
const consulKvList = consulKvListFactory(collection, clickable, attribute, deletable);
|
||||||
const consulTokenList = consulTokenListFactory(
|
const consulTokenList = consulTokenListFactory(
|
||||||
collection,
|
collection,
|
||||||
clickable,
|
clickable,
|
||||||
|
@ -149,7 +151,7 @@ export default {
|
||||||
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
|
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
|
||||||
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
|
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
|
||||||
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup, text)),
|
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup, text)),
|
||||||
kvs: create(kvs(visitable, deletable, creatable, clickable, attribute, collection)),
|
kvs: create(kvs(visitable, creatable, consulKvList)),
|
||||||
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
|
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
|
||||||
acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)),
|
acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)),
|
||||||
acl: create(acl(visitable, submitable, deletable, cancelable, clickable)),
|
acl: create(acl(visitable, submitable, deletable, cancelable, clickable)),
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
export default function(visitable, deletable, creatable, clickable, attribute, collection) {
|
export default function(visitable, creatable, kvs) {
|
||||||
return creatable({
|
return creatable({
|
||||||
visit: visitable(['/:dc/kv/:kv', '/:dc/kv'], str => str),
|
visit: visitable(['/:dc/kv/:kv', '/:dc/kv'], str => str),
|
||||||
kvs: collection(
|
kvs: kvs(),
|
||||||
'[data-test-tabular-row]',
|
|
||||||
deletable({
|
|
||||||
name: attribute('data-test-kv', '[data-test-kv]'),
|
|
||||||
kv: clickable('a'),
|
|
||||||
actions: clickable('label'),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupTest } from 'ember-qunit';
|
|
||||||
|
|
||||||
module('Unit | Controller | dc/kv/create', function(hooks) {
|
|
||||||
setupTest(hooks);
|
|
||||||
|
|
||||||
// Replace this with your real tests.
|
|
||||||
test('it exists', function(assert) {
|
|
||||||
let controller = this.owner.lookup('controller:dc/kv/create');
|
|
||||||
assert.ok(controller);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupTest } from 'ember-qunit';
|
|
||||||
|
|
||||||
module('Unit | Controller | dc/kv/edit', function(hooks) {
|
|
||||||
setupTest(hooks);
|
|
||||||
|
|
||||||
// Replace this with your real tests.
|
|
||||||
test('it exists', function(assert) {
|
|
||||||
let controller = this.owner.lookup('controller:dc/kv/edit');
|
|
||||||
assert.ok(controller);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupTest } from 'ember-qunit';
|
|
||||||
|
|
||||||
module('Unit | Controller | dc/kv/folder', function(hooks) {
|
|
||||||
setupTest(hooks);
|
|
||||||
|
|
||||||
// Replace this with your real tests.
|
|
||||||
test('it exists', function(assert) {
|
|
||||||
let controller = this.owner.lookup('controller:dc/kv/folder');
|
|
||||||
assert.ok(controller);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupTest } from 'ember-qunit';
|
|
||||||
|
|
||||||
module('Unit | Controller | dc/kv/root-create', function(hooks) {
|
|
||||||
setupTest(hooks);
|
|
||||||
|
|
||||||
// Replace this with your real tests.
|
|
||||||
test('it exists', function(assert) {
|
|
||||||
let controller = this.owner.lookup('controller:dc/kv/root-create');
|
|
||||||
assert.ok(controller);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,49 +0,0 @@
|
||||||
import { module, skip } from 'qunit';
|
|
||||||
import { setupTest } from 'ember-qunit';
|
|
||||||
import test from 'ember-sinon-qunit/test-support/test';
|
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import Mixin from 'consul-ui/mixins/kv/with-actions';
|
|
||||||
|
|
||||||
module('Unit | Mixin | kv/with actions', function(hooks) {
|
|
||||||
setupTest(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function() {
|
|
||||||
this.subject = function() {
|
|
||||||
const MixedIn = Route.extend(Mixin);
|
|
||||||
this.owner.register('test-container:kv/with-actions-object', MixedIn);
|
|
||||||
return this.owner.lookup('test-container:kv/with-actions-object');
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace this with your real tests.
|
|
||||||
test('it works', function(assert) {
|
|
||||||
const subject = this.subject();
|
|
||||||
assert.ok(subject);
|
|
||||||
});
|
|
||||||
test('afterUpdate calls transitionTo index when the key is a single slash', function(assert) {
|
|
||||||
const subject = this.subject();
|
|
||||||
const expected = 'dc.kv.index';
|
|
||||||
const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0);
|
|
||||||
const actual = subject.afterUpdate({}, { Key: '/' });
|
|
||||||
assert.equal(actual, expected);
|
|
||||||
assert.ok(transitionTo.calledOnce);
|
|
||||||
});
|
|
||||||
test('afterUpdate calls transitionTo folder when the key is not a single slash', function(assert) {
|
|
||||||
const subject = this.subject();
|
|
||||||
const expected = 'dc.kv.folder';
|
|
||||||
const transitionTo = this.stub(subject, 'transitionTo').returnsArg(0);
|
|
||||||
['', '/key', 'key/name'].forEach(item => {
|
|
||||||
const actual = subject.afterUpdate({}, { Key: item });
|
|
||||||
assert.equal(actual, expected);
|
|
||||||
});
|
|
||||||
assert.ok(transitionTo.calledThrice);
|
|
||||||
});
|
|
||||||
test('afterDelete calls refresh folder when the routeName is `folder`', function(assert) {
|
|
||||||
const subject = this.subject();
|
|
||||||
subject.routeName = 'dc.kv.folder';
|
|
||||||
const refresh = this.stub(subject, 'refresh');
|
|
||||||
subject.afterDelete({}, {});
|
|
||||||
assert.ok(refresh.calledOnce);
|
|
||||||
});
|
|
||||||
skip('action invalidateSession test');
|
|
||||||
});
|
|
Loading…
Reference in New Issue