diff --git a/ui/app/mixins/list-controller.js b/ui/app/mixins/list-controller.js index 423b42a12e21..dbe234cf5e13 100644 --- a/ui/app/mixins/list-controller.js +++ b/ui/app/mixins/list-controller.js @@ -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: { @@ -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: { diff --git a/ui/app/utils/common-prefix.js b/ui/app/utils/common-prefix.js new file mode 100644 index 000000000000..59d27b8d6318 --- /dev/null +++ b/ui/app/utils/common-prefix.js @@ -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); +} diff --git a/ui/lib/.eslintrc.js b/ui/lib/.eslintrc.js index 548ea343c9f0..02fab21e154c 100644 --- a/ui/lib/.eslintrc.js +++ b/ui/lib/.eslintrc.js @@ -1,6 +1,6 @@ module.exports = { env: { node: true, - browser: false, + browser: true, }, }; diff --git a/ui/tests/unit/utils/common-prefix-test.js b/ui/tests/unit/utils/common-prefix-test.js new file mode 100644 index 000000000000..5b437330c5ed --- /dev/null +++ b/ui/tests/unit/utils/common-prefix-test.js @@ -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'); + }); +});