Skip to content

Commit

Permalink
Change tab completion in the UI to prefer common prefix (#6759)
Browse files Browse the repository at this point in the history
* add common-prefix util and use it in the list controller

* add test

* browser js for in-repo dirs

* address PR feedback
  • Loading branch information
meirish authored May 22, 2019
1 parent 1a72f14 commit 8cdba29
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 12 deletions.
29 changes: 18 additions & 11 deletions ui/app/mixins/list-controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +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';

export default Mixin.create({
queryParams: {
Expand All @@ -16,21 +17,27 @@ export default Mixin.create({
isLoading: false,

filterMatchesKey: computed('filter', 'model', 'model.[]', function() {
var filter = this.get('filter');
var content = this.get('model');
let { filter, model: content } = this;
return !!(content.length && content.findBy('id', filter));
}),

firstPartialMatch: computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() {
var filter = this.get('filter');
var content = this.get('model');
var filterMatchesKey = this.get('filterMatchesKey');
var re = new RegExp('^' + escapeStringRegexp(filter));
return filterMatchesKey
? null
: content.find(function(key) {
return re.test(key.get('id'));
});
let { filter, filterMatchesKey, model: content } = this;
let re = new RegExp('^' + escapeStringRegexp(filter));
let matchSet = content.filter(key => re.test(key.id));
let match = matchSet[0];

if (filterMatchesKey || !match) {
return null;
}

let sharedPrefix = commonPrefix(content);
// if we already are filtering the prefix, then next we want
// the exact match
if (filter === sharedPrefix || matchSet.length === 1) {
return match;
}
return { id: sharedPrefix };
}),

actions: {
Expand Down
24 changes: 24 additions & 0 deletions ui/app/utils/common-prefix.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default function(arr = [], attribute = 'id') {
if (!arr.length) {
return '';
}
// this assumes an already sorted array
// if the array is sorted, we want to compare the first and last
// item in the array - if they share a prefix, all of the items do
let firstString = arr[0][attribute];
let lastString = arr[arr.length - 1][attribute];

// the longest the shared prefix could be is the length of the match
let targetLength = firstString.length;
let prefixLength = 0;
// walk the two strings, and if they match at the current length,
// increment the prefixLength and try again
while (
prefixLength < targetLength &&
firstString.charAt(prefixLength) === lastString.charAt(prefixLength)
) {
prefixLength++;
}
// slice the prefix from the first item
return firstString.substring(0, prefixLength);
}
2 changes: 1 addition & 1 deletion ui/lib/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
env: {
node: true,
browser: false,
browser: true,
},
};
32 changes: 32 additions & 0 deletions ui/tests/unit/utils/common-prefix-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import commonPrefix from 'vault/utils/common-prefix';
import { module, test } from 'qunit';

module('Unit | Util | common prefix', function() {
test('it returns empty string if called with no args or an empty array', function(assert) {
let returned = commonPrefix();
assert.equal(returned, '', 'returns an empty string');
returned = commonPrefix([]);
assert.equal(returned, '', 'returns an empty string for an empty array');
});

test('it returns empty string if there are no common prefixes', function(assert) {
let secrets = ['asecret', 'secret2', 'secret3'].map(s => ({ id: s }));
let returned = commonPrefix(secrets);
assert.equal(returned, '', 'returns an empty string');
});

test('it returns the longest prefix', function(assert) {
let secrets = ['secret1', 'secret2', 'secret3'].map(s => ({ id: s }));
let returned = commonPrefix(secrets);
assert.equal(returned, 'secret', 'finds secret prefix');
let greetings = ['hello-there', 'hello-hi', 'hello-howdy'].map(s => ({ id: s }));
returned = commonPrefix(greetings);
assert.equal(returned, 'hello-', 'finds hello- prefix');
});

test('it can compare an attribute that is not "id" to calculate the longest prefix', function(assert) {
let secrets = ['secret1', 'secret2', 'secret3'].map(s => ({ name: s }));
let returned = commonPrefix(secrets, 'name');
assert.equal(returned, 'secret', 'finds secret prefix from name attribute');
});
});

0 comments on commit 8cdba29

Please sign in to comment.