Glimmer Navigate Input component (#19517)
* wip * wip * todo hackeweek remove * clean up * add documetnation and fix test failure * pr review changes * spelling * remove unused method
This commit is contained in:
parent
65e5730c6c
commit
bd8d3d4e07
|
@ -16,7 +16,7 @@
|
|||
{{#if this.model.meta.total}}
|
||||
<ToolbarFilters>
|
||||
<NavigateInput
|
||||
filterFocusDidChange={{action "setFilterFocus"}}
|
||||
@filterFocusDidChange={{action "setFilterFocus"}}
|
||||
@filterDidChange={{action "setFilter"}}
|
||||
@filter={{this.filter}}
|
||||
@filterMatchesKey={{this.filterMatchesKey}}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<div class="navigate-filter">
|
||||
<div class="field" data-test-nav-input>
|
||||
<p class="control has-icons-left">
|
||||
{{! template-lint-disable no-down-event-binding }}
|
||||
<input
|
||||
class="filter input"
|
||||
value={{@filter}}
|
||||
placeholder={{or @placeholder "Filter items"}}
|
||||
type="text"
|
||||
data-test-component="navigate-input"
|
||||
{{on "input" this.handleInput}}
|
||||
{{on "keyup" this.handleKeyUp}}
|
||||
{{on "keydown" this.handleKeyPress}}
|
||||
{{on "focus" (fn this.setFilterFocused true)}}
|
||||
{{on "blur" (fn this.setFilterFocused false)}}
|
||||
/>
|
||||
<Icon @name="search" class="search-icon has-text-grey-light" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -1,14 +1,34 @@
|
|||
import { schedule, debounce } from '@ember/runloop';
|
||||
import { debounce } from '@ember/runloop';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Component from '@ember/component';
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
//TODO MOVE THESE TO THE ADDON
|
||||
// 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';
|
||||
|
||||
import layout from '../templates/components/navigate-input';
|
||||
/**
|
||||
* @module NavigateInput
|
||||
* `NavigateInput` components are used to filter list data.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <NavigateInput @filter={@roleFiltered} @placeholder="placeholder text" urls="{{hash list="vault.cluster.secrets.backend.kubernetes.roles"}}"/>
|
||||
* ```
|
||||
*
|
||||
* @param {String} filter=null - The filtered string.
|
||||
* @param {String} [placeholder="Filter items"] - The message inside the input to indicate what the user should enter into the space.
|
||||
* @param {Object} [urls=null] - An object containing list=route url.
|
||||
* @param {Function} [filterFocusDidChange=null] - A function called when the focus changes.
|
||||
* @param {Function} [filterDidChange=null] - A function called when the filter string changes.
|
||||
* @param {Function} [filterMatchesKey=null] - A function used to match to a specific key, such as an Id.
|
||||
* @param {Function} [filterPartialMatch=null] - A function used to filter through a partial match. Such as "oo" of "root".
|
||||
* @param {String} [baseKey=""] - A string to transition by Id.
|
||||
* @param {Boolean} [shouldNavigateTree=false] - If true, navigate a larger tree, such as when you're navigating leases under access.
|
||||
* @param {String} [mode="secrets"] - Mode which plays into navigation type.
|
||||
* @param {String} [extraNavParams=""] - A string used in route transition when necessary.
|
||||
*/
|
||||
|
||||
const routeFor = function (type, mode, urls) {
|
||||
const MODES = {
|
||||
|
@ -34,25 +54,12 @@ const routeFor = function (type, mode, urls) {
|
|||
return useSuffix ? modeVal + '.' + typeVal : modeVal;
|
||||
};
|
||||
|
||||
export default Component.extend(FocusOnInsertMixin, {
|
||||
layout,
|
||||
router: service(),
|
||||
export default class NavigateInput extends Component {
|
||||
@service router;
|
||||
|
||||
classNames: ['navigate-filter'],
|
||||
urls: null,
|
||||
|
||||
// these get passed in from the outside
|
||||
// actions that get passed in
|
||||
filterFocusDidChange: null,
|
||||
filterDidChange: null,
|
||||
mode: 'secrets',
|
||||
shouldNavigateTree: false,
|
||||
extraNavParams: null,
|
||||
|
||||
baseKey: null,
|
||||
filter: null,
|
||||
filterMatchesKey: null,
|
||||
firstPartialMatch: null,
|
||||
get mode() {
|
||||
return this.args.mode || 'secrets';
|
||||
}
|
||||
|
||||
transitionToRoute(...args) {
|
||||
const params = args.map((param, index) => {
|
||||
|
@ -63,43 +70,37 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||
});
|
||||
|
||||
this.router.transitionTo(...params);
|
||||
},
|
||||
|
||||
shouldFocus: false,
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
if (!this.filter) return;
|
||||
schedule('afterRender', this, 'forceFocus');
|
||||
},
|
||||
}
|
||||
|
||||
keyForNav(key) {
|
||||
if (this.mode !== 'secrets-cert') {
|
||||
return key;
|
||||
}
|
||||
return `cert/${key}`;
|
||||
},
|
||||
onEnter: function (val) {
|
||||
const { baseKey, mode } = this;
|
||||
const extraParams = this.extraNavParams;
|
||||
}
|
||||
|
||||
onEnter(val) {
|
||||
const mode = this.mode;
|
||||
const baseKey = this.args.baseKey;
|
||||
const extraParams = this.args.extraNavParams;
|
||||
if (mode.startsWith('secrets') && (!val || val === baseKey)) {
|
||||
return;
|
||||
}
|
||||
if (this.filterMatchesKey && !utils.keyIsFolder(val)) {
|
||||
const params = [routeFor('show', mode, this.urls), extraParams, this.keyForNav(val)].compact();
|
||||
if (this.args.filterMatchesKey && !utils.keyIsFolder(val)) {
|
||||
const params = [routeFor('show', mode, this.args.urls), extraParams, this.keyForNav(val)].compact();
|
||||
this.transitionToRoute(...params);
|
||||
} else {
|
||||
if (mode === 'policies') {
|
||||
return;
|
||||
}
|
||||
const route = routeFor('create', mode, this.urls);
|
||||
const route = routeFor('create', mode, this.args.urls);
|
||||
if (baseKey) {
|
||||
this.transitionToRoute(route, this.keyForNav(baseKey), {
|
||||
queryParams: {
|
||||
initialKey: val,
|
||||
},
|
||||
});
|
||||
} else if (this.urls) {
|
||||
} else if (this.args.urls) {
|
||||
this.transitionToRoute(route, {
|
||||
queryParams: {
|
||||
initialKey: this.keyForNav(val),
|
||||
|
@ -113,35 +114,35 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// pop to the nearest parentKey or to the root
|
||||
onEscape: function (val) {
|
||||
var key = utils.parentKeyForKey(val) || '';
|
||||
this.filterDidChange(key);
|
||||
onEscape(val) {
|
||||
const key = utils.parentKeyForKey(val) || '';
|
||||
this.args.filterDidChange(key);
|
||||
this.filterUpdated(key);
|
||||
},
|
||||
}
|
||||
|
||||
onTab: function (event) {
|
||||
var firstPartialMatch = this.firstPartialMatch.id;
|
||||
onTab(event) {
|
||||
const firstPartialMatch = this.args.firstPartialMatch.id;
|
||||
if (!firstPartialMatch) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.filterDidChange(firstPartialMatch);
|
||||
this.args.filterDidChange(firstPartialMatch);
|
||||
this.filterUpdated(firstPartialMatch);
|
||||
},
|
||||
}
|
||||
|
||||
// as you type, navigates through the k/v tree
|
||||
filterUpdated: function (val) {
|
||||
var mode = this.mode;
|
||||
if (mode === 'policies' || !this.shouldNavigateTree) {
|
||||
filterUpdated(val) {
|
||||
const mode = this.mode;
|
||||
if (mode === 'policies' || !this.args.shouldNavigateTree) {
|
||||
this.filterUpdatedNoNav(val, mode);
|
||||
return;
|
||||
}
|
||||
// select the key to nav to, assumed to be a folder
|
||||
var key = val ? val.trim() : '';
|
||||
var isFolder = utils.keyIsFolder(key);
|
||||
let key = val ? val.trim() : '';
|
||||
const isFolder = utils.keyIsFolder(key);
|
||||
|
||||
if (!isFolder) {
|
||||
// nav to the closest parentKey (or the root)
|
||||
|
@ -150,10 +151,10 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||
|
||||
const pageFilter = val.replace(key, '');
|
||||
this.navigate(this.keyForNav(key), mode, pageFilter);
|
||||
},
|
||||
}
|
||||
|
||||
navigate(key, mode, pageFilter) {
|
||||
const route = routeFor(key ? 'list' : 'list-root', mode, this.urls);
|
||||
const route = routeFor(key ? 'list' : 'list-root', mode, this.args.urls);
|
||||
const args = [route];
|
||||
if (key) {
|
||||
args.push(key);
|
||||
|
@ -174,47 +175,46 @@ export default Component.extend(FocusOnInsertMixin, {
|
|||
});
|
||||
}
|
||||
this.transitionToRoute(...args);
|
||||
},
|
||||
}
|
||||
|
||||
filterUpdatedNoNav: function (val, mode) {
|
||||
var key = val ? val.trim() : null;
|
||||
this.transitionToRoute(routeFor('list-root', mode, this.urls), {
|
||||
filterUpdatedNoNav(val, mode) {
|
||||
const key = val ? val.trim() : null;
|
||||
this.transitionToRoute(routeFor('list-root', mode, this.args.urls), {
|
||||
queryParams: {
|
||||
pageFilter: key,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
actions: {
|
||||
handleInput: function (filter) {
|
||||
if (this.filterDidChange) {
|
||||
this.filterDidChange(filter);
|
||||
}
|
||||
debounce(this, 'filterUpdated', filter, 200);
|
||||
},
|
||||
|
||||
setFilterFocused: function (isFocused) {
|
||||
if (this.filterFocusDidChange) {
|
||||
this.filterFocusDidChange(isFocused);
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyPress: function (event) {
|
||||
if (event.keyCode === keys.TAB) {
|
||||
this.onTab(event);
|
||||
}
|
||||
},
|
||||
|
||||
handleKeyUp: function (event) {
|
||||
var keyCode = event.keyCode;
|
||||
const val = event.target.value;
|
||||
if (keyCode === keys.ENTER) {
|
||||
this.onEnter(val);
|
||||
}
|
||||
if (keyCode === keys.ESC) {
|
||||
this.onEscape(val);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@action
|
||||
handleInput(filter) {
|
||||
if (this.args.filterDidChange) {
|
||||
this.args.filterDidChange(filter.target.value);
|
||||
}
|
||||
debounce(this, this.filterUpdated, filter.target.value, 200);
|
||||
}
|
||||
@action
|
||||
setFilterFocused(isFocused) {
|
||||
if (this.args.filterFocusDidChange) {
|
||||
this.args.filterFocusDidChange(isFocused);
|
||||
}
|
||||
}
|
||||
@action
|
||||
handleKeyPress(event) {
|
||||
if (event.keyCode === keys.TAB) {
|
||||
this.onTab(event);
|
||||
}
|
||||
}
|
||||
@action
|
||||
handleKeyUp(event) {
|
||||
const keyCode = event.keyCode;
|
||||
const val = event.target.value;
|
||||
if (keyCode === keys.ENTER) {
|
||||
this.onEnter(val);
|
||||
}
|
||||
if (keyCode === keys.ESC) {
|
||||
this.onEscape(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<div class="field" data-test-nav-input>
|
||||
<p class="control has-icons-left">
|
||||
{{! template-lint-disable no-down-event-binding }}
|
||||
<input
|
||||
class="filter input"
|
||||
disabled={{this.disabled}}
|
||||
value={{@filter}}
|
||||
placeholder={{or @placeholder "Filter keys"}}
|
||||
type="text"
|
||||
data-test-comoponent="navigate-input"
|
||||
oninput={{action "handleInput" value="target.value"}}
|
||||
onkeyup={{action "handleKeyUp"}}
|
||||
onkeydown={{action "handleKeyPress"}}
|
||||
onfocus={{action "setFilterFocused" true}}
|
||||
onblur={{action "setFilterFocused" false}}
|
||||
/>
|
||||
{{! template-lint-enable no-down-event-binding }}
|
||||
<Icon @name="search" class="search-icon has-text-grey-light" />
|
||||
</p>
|
||||
</div>
|
|
@ -12,7 +12,7 @@
|
|||
<Toolbar>
|
||||
{{#if this.model.certificates.length}}
|
||||
<ToolbarFilters>
|
||||
{{! ARG TODO glimmerize the NavigateInput and refactor so you can use it in an engine }}
|
||||
{{! TODO add NavigateInput component }}
|
||||
</ToolbarFilters>
|
||||
{{/if}}
|
||||
</Toolbar>
|
||||
|
|
|
@ -27,6 +27,7 @@ module('Acceptance | aws secret backend', function (hooks) {
|
|||
],
|
||||
};
|
||||
test('aws backend', async function (assert) {
|
||||
assert.expect(12);
|
||||
const now = new Date().getTime();
|
||||
const path = `aws-${now}`;
|
||||
const roleName = 'awsrole';
|
||||
|
@ -91,13 +92,10 @@ module('Acceptance | aws secret backend', function (hooks) {
|
|||
assert.ok(findAll(`[data-test-secret-link="${roleName}"]`).length, `aws: role shows in the list`);
|
||||
|
||||
//and delete
|
||||
// TODO the button does not populate quickly enough.
|
||||
// await click(`[data-test-secret-link="${roleName}"] [data-test-popup-menu-trigger]`);
|
||||
// await settled();
|
||||
// await click(`[data-test-aws-role-delete="${roleName}"]`);
|
||||
|
||||
// await click(`[data-test-confirm-button]`);
|
||||
// await settled();
|
||||
// assert.dom(`[data-test-secret-link="${roleName}"]`).doesNotExist(`aws: role is no longer in the list`);
|
||||
await click(`[data-test-secret-link="${roleName}"] [data-test-popup-menu-trigger]`);
|
||||
await waitUntil(() => find(`[data-test-aws-role-delete="${roleName}"]`)); // flaky without
|
||||
await click(`[data-test-aws-role-delete="${roleName}"]`);
|
||||
await click(`[data-test-confirm-button]`);
|
||||
assert.dom(`[data-test-secret-link="${roleName}"]`).doesNotExist(`aws: role is no longer in the list`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { click, currentRouteName, visit } from '@ember/test-helpers';
|
||||
// TESTS HERE ARE SKPPED
|
||||
// TESTS HERE ARE SKIPPED
|
||||
// running vault with -dev-leased-kv flag lets you run some of these tests
|
||||
// but generating leases programmatically is currently difficult
|
||||
//
|
||||
|
|
|
@ -30,7 +30,7 @@ module('Acceptance | kubernetes | roles', function (hooks) {
|
|||
test('it should filter roles', async function (assert) {
|
||||
await this.visitRoles();
|
||||
assert.dom('[data-test-list-item-link]').exists({ count: 3 }, 'Roles list renders');
|
||||
await fillIn('[data-test-comoponent="navigate-input"]', '1');
|
||||
await fillIn('[data-test-component="navigate-input"]', '1');
|
||||
assert.dom('[data-test-list-item-link]').exists({ count: 1 }, 'Filtered roles list renders');
|
||||
assert.ok(currentURL().includes('pageFilter=1'), 'pageFilter query param value is set');
|
||||
});
|
||||
|
|
|
@ -75,7 +75,7 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||
},
|
||||
];
|
||||
test('ssh backend', async function (assert) {
|
||||
assert.expect(26);
|
||||
assert.expect(28);
|
||||
const now = new Date().getTime();
|
||||
const sshPath = `ssh-${now}`;
|
||||
|
||||
|
@ -157,17 +157,13 @@ module('Acceptance | ssh secret backend', function (hooks) {
|
|||
);
|
||||
|
||||
//and delete
|
||||
// TODO confirmed functionality works, but it can not find the data-test-ssh-role-delete in time.
|
||||
// await click(`[data-test-secret-link="${role.name}"] [data-test-popup-menu-trigger]`);
|
||||
// await settled();
|
||||
// await click(`[data-test-ssh-role-delete]`);
|
||||
// await settled();
|
||||
// await click(`[data-test-confirm-button]`);
|
||||
|
||||
// await settled();
|
||||
// assert
|
||||
// .dom(`[data-test-secret-link="${role.name}"]`)
|
||||
// .doesNotExist(`${role.type}: role is no longer in the list`);
|
||||
await click(`[data-test-secret-link="${role.name}"] [data-test-popup-menu-trigger]`);
|
||||
await waitUntil(() => find('[data-test-ssh-role-delete]')); // flaky without
|
||||
await click(`[data-test-ssh-role-delete]`);
|
||||
await click(`[data-test-confirm-button]`);
|
||||
assert
|
||||
.dom(`[data-test-secret-link="${role.name}"]`)
|
||||
.doesNotExist(`${role.type}: role is no longer in the list`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -88,7 +88,7 @@ module('Integration | Component | auth form', function (hooks) {
|
|||
this.set('cluster', EmberObject.create({}));
|
||||
this.set('selectedAuth', 'token');
|
||||
await render(hbs`{{auth-form cluster=this.cluster selectedAuth=this.selectedAuth}}`);
|
||||
// ARG TODO research and see if adapter errors changed, but null used to be Bad Request
|
||||
// returns null because test does not return details of failed network request. On the app it will return the details of the error instead of null.
|
||||
return component.login().then(() => {
|
||||
assert.strictEqual(component.errorText, 'Error Authentication failed: null');
|
||||
server.shutdown();
|
||||
|
|
|
@ -6,9 +6,7 @@ export default create({
|
|||
visit: visitable('/vault/secrets/:backend/kmip/scopes/:scope/roles/:role/credentials'),
|
||||
visitDetail: visitable('/vault/secrets/:backend/kmip/scopes/:scope/roles/:role/credentials/:serial'),
|
||||
create: clickable('[data-test-role-create]'),
|
||||
credentialsLink: clickable('[data-test-kmip-link-credentials]'),
|
||||
generateCredentialsLink: clickable('[data-test-kmip-link-generate-credentials]'),
|
||||
roleDetailsLink: clickable('[data-test-kmip-link-role-details]'),
|
||||
backToRoleLink: clickable('[data-test-kmip-link-back-to-role]'),
|
||||
submit: clickable('[data-test-edit-form-submit]'),
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue