From 885a59295b45fc3e00123e18dbedd8ea5ee24316 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 09:08:31 -0500 Subject: [PATCH 01/25] Remove running client-side search tests for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’ll delete these later after I’ve replaced them. --- ui/tests/acceptance/search-test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index 10d9f7aa6fb..632d1f1df82 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -1,5 +1,5 @@ /* eslint-disable ember-a11y-testing/a11y-audit-called */ // TODO -import { module, test } from 'qunit'; +import { module, skip, test } from 'qunit'; import { currentURL, triggerEvent, visit } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -18,7 +18,7 @@ module('Acceptance | search', function(hooks) { setupApplicationTest(hooks); setupMirage(hooks); - test('search searches jobs and nodes with route- and time-based caching and navigates to chosen items', async function(assert) { + skip('search searches jobs and nodes with route- and time-based caching and navigates to chosen items', async function(assert) { server.create('node', { name: 'xyz' }); const otherNode = server.create('node', { name: 'ghi' }); @@ -103,7 +103,7 @@ module('Acceptance | search', function(hooks) { clock.restore(); }); - test('search highlights matching substrings', async function(assert) { + skip('search highlights matching substrings', async function(assert) { server.create('node', { name: 'xyz' }); server.create('job', { id: 'traefik', namespaceId: 'default' }); @@ -142,7 +142,7 @@ module('Acceptance | search', function(hooks) { }); }); - test('results are truncated at 10 per group', async function(assert) { + skip('results are truncated at 10 per group', async function(assert) { server.create('node', { name: 'xyz' }); for (let i = 0; i < 15; i++) { @@ -161,7 +161,7 @@ module('Acceptance | search', function(hooks) { }); }); - test('node id prefix matches take priority over node name matches', async function(assert) { + skip('node id prefix matches take priority over node name matches', async function(assert) { const nodeToMatchById = server.create('node', { name: 'xyz' }); const idPrefix = nodeToMatchById.id.substr(0, 5); From 601d1a66c01d689afcb38098101b922d5b018fa2 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 09:38:23 -0500 Subject: [PATCH 02/25] Add preliminary fuzzy search --- ui/app/components/global-search/control.js | 28 +++++++++++++++++++ .../components/global-search/control.hbs | 2 +- ui/mirage/config.js | 18 ++++++++++++ ui/tests/acceptance/search-test.js | 23 +++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js index a72f2ef1c31..d9c9c5c506b 100644 --- a/ui/app/components/global-search/control.js +++ b/ui/app/components/global-search/control.js @@ -16,6 +16,7 @@ export default class GlobalSearchControl extends Component { @service dataCaches; @service router; @service store; + @service token; searchString = null; @@ -86,6 +87,33 @@ export default class GlobalSearchControl extends Component { console.log('exception searching', e); } }) + searchOld; + + @task(function*(string) { + const searchResponse = yield this.token.authorizedRequest('/v1/search/fuzzy', { + method: 'POST', + body: JSON.stringify({ + Text: string, + Context: 'all', + }), + }); + + const results = yield searchResponse.json(); + + const jobResults = results.Matches.jobs || []; + const nodeResults = results.Matches.nodes || []; + + return [ + { + groupName: resultsGroupLabel('Jobs', jobResults, [] /* FIXME */), + options: jobResults, + }, + { + groupName: resultsGroupLabel('Clients', nodeResults, [] /* FIXME */), + options: nodeResults, + }, + ]; + }) search; @action diff --git a/ui/app/templates/components/global-search/control.hbs b/ui/app/templates/components/global-search/control.hbs index 4c1226b4729..950b881dedd 100644 --- a/ui/app/templates/components/global-search/control.hbs +++ b/ui/app/templates/components/global-search/control.hbs @@ -12,5 +12,5 @@ @triggerComponent="global-search/trigger" @registerAPI={{action 'storeSelect'}} as |option|> - + {{option.ID}} diff --git a/ui/mirage/config.js b/ui/mirage/config.js index f739a353b69..d8646177f6e 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -568,6 +568,24 @@ export default function() { }); }); + this.post('/search/fuzzy', function( { jobs }, { requestBody }) { + const { Text } = JSON.parse(requestBody); + + const matchedJobs = jobs.where(job => job.name.includes(Text)); + + return { + Matches: { + jobs: matchedJobs.models.map(job => ({ + ID: job.name, + Scope: [ + job.id, + job.namespace, + ] + })) + } + } + }); + this.get('/recommendations', function( { jobs, namespaces, recommendations }, { queryParams: { job: id, namespace } } diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index 632d1f1df82..2c8b67de9dd 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -18,6 +18,29 @@ module('Acceptance | search', function(hooks) { setupApplicationTest(hooks); setupMirage(hooks); + test('search exposes results from the fuzzy search endpoint', async function(assert) { + server.create('node'); + + server.create('job', { id: 'vwxyz', namespaceId: 'default' }); + server.create('job', { id: 'xyz', name: 'xyz job', namespace: 'default' }); + server.create('job', { id: 'abc', namespace: 'default' }); + + await visit('/'); + + await selectSearch(Layout.navbar.search.scope, 'xy'); + + Layout.navbar.search.as(search => { + assert.equal(search.groups.length, 2); + + search.groups[0].as(jobs => { + assert.equal(jobs.name, 'Jobs (2)'); + assert.equal(jobs.options.length, 2); + assert.equal(jobs.options[0].text, 'vwxyz'); + assert.equal(jobs.options[1].text, 'xyz job'); + }); + }); + }); + skip('search searches jobs and nodes with route- and time-based caching and navigates to chosen items', async function(assert) { server.create('node', { name: 'xyz' }); const otherNode = server.create('node', { name: 'ghi' }); From 555ba2dbfd47186d46f07118fbba03595354126f Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 09:46:47 -0500 Subject: [PATCH 03/25] Add placeholder for ignoring one-character search --- ui/tests/acceptance/search-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index 2c8b67de9dd..f23b5987052 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -41,6 +41,8 @@ module('Acceptance | search', function(hooks) { }); }); + skip('search only triggers after two characters have been entered'); + skip('search searches jobs and nodes with route- and time-based caching and navigates to chosen items', async function(assert) { server.create('node', { name: 'xyz' }); const otherNode = server.create('node', { name: 'ghi' }); From 4501e44432dcf326ff32e27dfdb2115117c285f4 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 09:52:50 -0500 Subject: [PATCH 04/25] Add navigation to jobs --- ui/app/components/global-search/control.js | 12 +++--------- ui/mirage/config.js | 2 +- ui/tests/acceptance/search-test.js | 9 ++++++--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js index d9c9c5c506b..c6dc62f71be 100644 --- a/ui/app/components/global-search/control.js +++ b/ui/app/components/global-search/control.js @@ -125,15 +125,9 @@ export default class GlobalSearchControl extends Component { @action selectOption(model) { - const itemModelName = model.constructor.modelName; - - if (itemModelName === 'job') { - this.router.transitionTo('jobs.job', model.plainId, { - queryParams: { namespace: model.get('namespace.name') }, - }); - } else if (itemModelName === 'node') { - this.router.transitionTo('clients.client', model.id); - } + this.router.transitionTo('jobs.job', model.Scope[1], { + queryParams: { namespace: model.Scope[0] }, + }); } @action diff --git a/ui/mirage/config.js b/ui/mirage/config.js index d8646177f6e..972bb0d7ec8 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -578,8 +578,8 @@ export default function() { jobs: matchedJobs.models.map(job => ({ ID: job.name, Scope: [ - job.id, job.namespace, + job.id, ] })) } diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index f23b5987052..e98296d7fcd 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -18,12 +18,12 @@ module('Acceptance | search', function(hooks) { setupApplicationTest(hooks); setupMirage(hooks); - test('search exposes results from the fuzzy search endpoint', async function(assert) { + test('search exposes and navigates to results from the fuzzy search endpoint', async function(assert) { server.create('node'); server.create('job', { id: 'vwxyz', namespaceId: 'default' }); - server.create('job', { id: 'xyz', name: 'xyz job', namespace: 'default' }); - server.create('job', { id: 'abc', namespace: 'default' }); + server.create('job', { id: 'xyz', name: 'xyz job', namespaceId: 'default' }); + server.create('job', { id: 'abc', namespaceId: 'default' }); await visit('/'); @@ -39,6 +39,9 @@ module('Acceptance | search', function(hooks) { assert.equal(jobs.options[1].text, 'xyz job'); }); }); + + await Layout.navbar.search.groups[0].options[1].click(); + assert.equal(currentURL(), '/jobs/xyz'); }); skip('search only triggers after two characters have been entered'); From 7d5fdf2879b3e27c8ee5ff2edce451b1a4f011c0 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 10:12:37 -0500 Subject: [PATCH 05/25] Add handling for node results --- ui/app/components/global-search/control.js | 13 ++++++++++--- ui/mirage/config.js | 9 ++++++++- ui/tests/acceptance/search-test.js | 14 +++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js index c6dc62f71be..9462a0af86e 100644 --- a/ui/app/components/global-search/control.js +++ b/ui/app/components/global-search/control.js @@ -103,6 +103,9 @@ export default class GlobalSearchControl extends Component { const jobResults = results.Matches.jobs || []; const nodeResults = results.Matches.nodes || []; + jobResults.forEach(job => job.type = 'job'); + nodeResults.forEach(node => node.type = 'node'); + return [ { groupName: resultsGroupLabel('Jobs', jobResults, [] /* FIXME */), @@ -125,9 +128,13 @@ export default class GlobalSearchControl extends Component { @action selectOption(model) { - this.router.transitionTo('jobs.job', model.Scope[1], { - queryParams: { namespace: model.Scope[0] }, - }); + if (model.type === 'job') { + this.router.transitionTo('jobs.job', model.Scope[1], { + queryParams: { namespace: model.Scope[0] }, + }); + } else if (model.type === 'node') { + this.router.transitionTo('clients.client', model.Scope[0]); + } } @action diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 972bb0d7ec8..5980662ccd8 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -568,10 +568,11 @@ export default function() { }); }); - this.post('/search/fuzzy', function( { jobs }, { requestBody }) { + this.post('/search/fuzzy', function( { jobs, nodes }, { requestBody }) { const { Text } = JSON.parse(requestBody); const matchedJobs = jobs.where(job => job.name.includes(Text)); + const matchedNodes = nodes.where(node => node.name.includes(Text)); return { Matches: { @@ -581,6 +582,12 @@ export default function() { job.namespace, job.id, ] + })), + nodes: matchedNodes.models.map(node => ({ + ID: node.name, + Scope: [ + node.id, + ], })) } } diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index e98296d7fcd..313e56fe233 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -19,7 +19,8 @@ module('Acceptance | search', function(hooks) { setupMirage(hooks); test('search exposes and navigates to results from the fuzzy search endpoint', async function(assert) { - server.create('node'); + server.create('node', { name: 'xyz' }); + const otherNode = server.create('node', { name: 'ghi' }); server.create('job', { id: 'vwxyz', namespaceId: 'default' }); server.create('job', { id: 'xyz', name: 'xyz job', namespaceId: 'default' }); @@ -38,10 +39,21 @@ module('Acceptance | search', function(hooks) { assert.equal(jobs.options[0].text, 'vwxyz'); assert.equal(jobs.options[1].text, 'xyz job'); }); + + search.groups[1].as(clients => { + assert.equal(clients.name, 'Clients (1)'); + assert.equal(clients.options.length, 1); + assert.equal(clients.options[0].text, 'xyz'); + }); }); await Layout.navbar.search.groups[0].options[1].click(); assert.equal(currentURL(), '/jobs/xyz'); + + await selectSearch(Layout.navbar.search.scope, otherNode.name); + + await Layout.navbar.search.groups[1].options[0].click(); + assert.equal(currentURL(), `/clients/${otherNode.id}`); }); skip('search only triggers after two characters have been entered'); From 5beff76d616092a6c30badbe2ac73f9e04cf0846 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 10:25:47 -0500 Subject: [PATCH 06/25] Add minimum search string length filter --- ui/app/components/global-search/control.js | 5 +++++ ui/app/templates/components/global-search/control.hbs | 1 + ui/tests/acceptance/search-test.js | 9 ++++++++- ui/tests/pages/layout.js | 6 ++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js index 9462a0af86e..67d95c604ec 100644 --- a/ui/app/components/global-search/control.js +++ b/ui/app/components/global-search/control.js @@ -126,6 +126,11 @@ export default class GlobalSearchControl extends Component { } } + @action + ensureMinimumLength(string) { + return string.length > 1; + } + @action selectOption(model) { if (model.type === 'job') { diff --git a/ui/app/templates/components/global-search/control.hbs b/ui/app/templates/components/global-search/control.hbs index 950b881dedd..b7f8aa0ccd4 100644 --- a/ui/app/templates/components/global-search/control.hbs +++ b/ui/app/templates/components/global-search/control.hbs @@ -3,6 +3,7 @@ data-test-search @searchEnabled={{true}} @search={{perform this.search}} + @onInput={{action 'ensureMinimumLength'}} @onChange={{action 'selectOption'}} @onFocus={{action 'openOnClickOrTab'}} @onClose={{action 'onCloseEvent'}} diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index 313e56fe233..3f4d76a3ca7 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -56,7 +56,14 @@ module('Acceptance | search', function(hooks) { assert.equal(currentURL(), `/clients/${otherNode.id}`); }); - skip('search only triggers after two characters have been entered'); + test('search does not perform a request when only one character has been entered', async function(assert) { + await visit('/'); + + await selectSearch(Layout.navbar.search.scope, 'q'); + + assert.ok(Layout.navbar.search.noOptionsShown); + assert.notOk(server.pretender.handledRequests.findBy('url', '/v1/search/fuzzy')); + }); skip('search searches jobs and nodes with route- and time-based caching and navigates to chosen items', async function(assert) { server.create('node', { name: 'xyz' }); diff --git a/ui/tests/pages/layout.js b/ui/tests/pages/layout.js index cb1fcbbe8c2..432f949a8b3 100644 --- a/ui/tests/pages/layout.js +++ b/ui/tests/pages/layout.js @@ -4,6 +4,7 @@ import { clickable, collection, hasClass, + isHidden, isPresent, text, } from 'ember-cli-page-object'; @@ -55,6 +56,11 @@ export default create({ ), }), + noOptionsShown: isHidden('.ember-power-select-options', { + testContainer: '.ember-basic-dropdown-content', + resetScope: true, + }), + field: { scope: '.ember-power-select-search input', testContainer: 'html', From 8ce3cb3ef5bf78abcab3cddd0868de7fa228eb33 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 10:59:18 -0500 Subject: [PATCH 07/25] Add handling for task group results --- ui/app/components/global-search/control.js | 10 +++++++++ ui/mirage/config.js | 10 ++++++++- ui/tests/acceptance/search-test.js | 26 +++++++++++++++++----- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js index 67d95c604ec..3d16726b9ac 100644 --- a/ui/app/components/global-search/control.js +++ b/ui/app/components/global-search/control.js @@ -102,9 +102,11 @@ export default class GlobalSearchControl extends Component { const jobResults = results.Matches.jobs || []; const nodeResults = results.Matches.nodes || []; + const groupResults = results.Matches.groups || []; jobResults.forEach(job => job.type = 'job'); nodeResults.forEach(node => node.type = 'node'); + groupResults.forEach(group => group.type = 'group'); return [ { @@ -115,6 +117,10 @@ export default class GlobalSearchControl extends Component { groupName: resultsGroupLabel('Clients', nodeResults, [] /* FIXME */), options: nodeResults, }, + { + groupName: resultsGroupLabel('Task Groups', groupResults, []), + options: groupResults, + }, ]; }) search; @@ -139,6 +145,10 @@ export default class GlobalSearchControl extends Component { }); } else if (model.type === 'node') { this.router.transitionTo('clients.client', model.Scope[0]); + } else if (model.type === 'group') { + this.router.transitionTo('jobs.job.task-group', model.Scope[1], model.ID, { + queryParams: { namespace: model.Scope[0] }, + }); } } diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 5980662ccd8..7728c827070 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -568,14 +568,22 @@ export default function() { }); }); - this.post('/search/fuzzy', function( { jobs, nodes }, { requestBody }) { + this.post('/search/fuzzy', function( { jobs, nodes, taskGroups }, { requestBody }) { const { Text } = JSON.parse(requestBody); + const matchedGroups = taskGroups.where(taskGroup => taskGroup.name.includes(Text)); const matchedJobs = jobs.where(job => job.name.includes(Text)); const matchedNodes = nodes.where(node => node.name.includes(Text)); return { Matches: { + groups: matchedGroups.models.map(group => ({ + ID: group.name, + Scope: [ + group.job.namespace, + group.job.id, + ], + })), jobs: matchedJobs.models.map(job => ({ ID: job.name, Scope: [ diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index 3f4d76a3ca7..cf4f5b17a59 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -22,16 +22,22 @@ module('Acceptance | search', function(hooks) { server.create('node', { name: 'xyz' }); const otherNode = server.create('node', { name: 'ghi' }); - server.create('job', { id: 'vwxyz', namespaceId: 'default' }); - server.create('job', { id: 'xyz', name: 'xyz job', namespaceId: 'default' }); - server.create('job', { id: 'abc', namespaceId: 'default' }); + server.create('job', { id: 'vwxyz', namespaceId: 'default', groupsCount: 1 }); + server.create('job', { id: 'xyz', name: 'xyz job', namespaceId: 'default', groupsCount: 1 }); + server.create('job', { id: 'abc', namespaceId: 'default', groupsCount: 1 }); + + // Override random task group names to include job names to make results deterministic + server.schema.taskGroups.all().models.forEach(group => { + group.name = `${group.job.name}-group`; + group.save(); + }); await visit('/'); await selectSearch(Layout.navbar.search.scope, 'xy'); Layout.navbar.search.as(search => { - assert.equal(search.groups.length, 2); + assert.equal(search.groups.length, 3); search.groups[0].as(jobs => { assert.equal(jobs.name, 'Jobs (2)'); @@ -45,15 +51,25 @@ module('Acceptance | search', function(hooks) { assert.equal(clients.options.length, 1); assert.equal(clients.options[0].text, 'xyz'); }); + + search.groups[2].as(groups => { + assert.equal(groups.name, 'Task Groups (2)'); + assert.equal(groups.options.length, 2); + assert.equal(groups.options[0].text, 'vwxyz-group'); + assert.equal(groups.options[1].text, 'xyz job-group'); + }); }); await Layout.navbar.search.groups[0].options[1].click(); assert.equal(currentURL(), '/jobs/xyz'); await selectSearch(Layout.navbar.search.scope, otherNode.name); - await Layout.navbar.search.groups[1].options[0].click(); assert.equal(currentURL(), `/clients/${otherNode.id}`); + + await selectSearch(Layout.navbar.search.scope, 'xy'); + await Layout.navbar.search.groups[2].options[0].click(); + assert.equal(currentURL(), '/jobs/vwxyz/vwxyz-group'); }); test('search does not perform a request when only one character has been entered', async function(assert) { From 3dadcada8a30314f463fbc7cad3b95161b23a2c7 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 15:40:20 -0500 Subject: [PATCH 08/25] Add client-side truncation --- ui/app/components/global-search/control.js | 16 ++++++++++------ ui/tests/acceptance/search-test.js | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js index 3d16726b9ac..1cfda79b625 100644 --- a/ui/app/components/global-search/control.js +++ b/ui/app/components/global-search/control.js @@ -100,9 +100,13 @@ export default class GlobalSearchControl extends Component { const results = yield searchResponse.json(); - const jobResults = results.Matches.jobs || []; - const nodeResults = results.Matches.nodes || []; - const groupResults = results.Matches.groups || []; + const allJobResults = results.Matches.jobs || []; + const allNodeResults = results.Matches.nodes || []; + const allGroupResults = results.Matches.groups || []; + + const jobResults = allJobResults.slice(0, MAXIMUM_RESULTS); + const nodeResults = allNodeResults.slice(0, MAXIMUM_RESULTS); + const groupResults = allGroupResults.slice(0, MAXIMUM_RESULTS); jobResults.forEach(job => job.type = 'job'); nodeResults.forEach(node => node.type = 'node'); @@ -110,15 +114,15 @@ export default class GlobalSearchControl extends Component { return [ { - groupName: resultsGroupLabel('Jobs', jobResults, [] /* FIXME */), + groupName: resultsGroupLabel('Jobs', jobResults, allJobResults), options: jobResults, }, { - groupName: resultsGroupLabel('Clients', nodeResults, [] /* FIXME */), + groupName: resultsGroupLabel('Clients', nodeResults, allNodeResults), options: nodeResults, }, { - groupName: resultsGroupLabel('Task Groups', groupResults, []), + groupName: resultsGroupLabel('Task Groups', groupResults, allGroupResults), options: groupResults, }, ]; diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index cf4f5b17a59..a9c8cd595d9 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -166,6 +166,25 @@ module('Acceptance | search', function(hooks) { clock.restore(); }); + test('results are truncated at 10 per group', async function(assert) { + server.create('node', { name: 'xyz' }); + + for (let i = 0; i < 11; i++) { + server.create('job', { id: `job-${i}`, namespaceId: 'default' }); + } + + await visit('/'); + + await selectSearch(Layout.navbar.search.scope, 'job'); + + Layout.navbar.search.as(search => { + search.groups[0].as(jobs => { + assert.equal(jobs.name, 'Jobs (showing 10 of 11)'); + assert.equal(jobs.options.length, 10); + }); + }); + }); + skip('search highlights matching substrings', async function(assert) { server.create('node', { name: 'xyz' }); From cf488fab0d1a0873ec62846bc933318197b4ca59 Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Tue, 20 Apr 2021 15:51:05 -0500 Subject: [PATCH 09/25] Add indicator for server-side truncation --- ui/app/components/global-search/control.js | 14 ++++-- ui/mirage/config.js | 57 ++++++++++++++-------- ui/tests/acceptance/search-test.js | 18 +++++++ 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/ui/app/components/global-search/control.js b/ui/app/components/global-search/control.js index 1cfda79b625..e5441af40e4 100644 --- a/ui/app/components/global-search/control.js +++ b/ui/app/components/global-search/control.js @@ -112,17 +112,19 @@ export default class GlobalSearchControl extends Component { nodeResults.forEach(node => node.type = 'node'); groupResults.forEach(group => group.type = 'group'); + const truncations = results.Truncations; + return [ { - groupName: resultsGroupLabel('Jobs', jobResults, allJobResults), + groupName: resultsGroupLabel('Jobs', jobResults, allJobResults, truncations.jobs), options: jobResults, }, { - groupName: resultsGroupLabel('Clients', nodeResults, allNodeResults), + groupName: resultsGroupLabel('Clients', nodeResults, allNodeResults, truncations.nodes), options: nodeResults, }, { - groupName: resultsGroupLabel('Task Groups', groupResults, allGroupResults), + groupName: resultsGroupLabel('Task Groups', groupResults, allGroupResults, truncations.groups), options: groupResults, }, ]; @@ -252,7 +254,7 @@ class NodeIdSearch extends EmberObject.extend(Searchable) { regexEnabled = true; } -function resultsGroupLabel(type, renderedResults, allResults) { +function resultsGroupLabel(type, renderedResults, allResults, truncated) { let countString; if (renderedResults.length < allResults.length) { @@ -261,5 +263,7 @@ function resultsGroupLabel(type, renderedResults, allResults) { countString = renderedResults.length; } - return `${type} (${countString})`; + const truncationIndicator = truncated ? '+' : ''; + + return `${type} (${countString}${truncationIndicator})`; } diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 7728c827070..34edb93ba0f 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -575,29 +575,44 @@ export default function() { const matchedJobs = jobs.where(job => job.name.includes(Text)); const matchedNodes = nodes.where(node => node.name.includes(Text)); + const transformedGroups = matchedGroups.models.map(group => ({ + ID: group.name, + Scope: [ + group.job.namespace, + group.job.id, + ], + })); + + const transformedJobs = matchedJobs.models.map(job => ({ + ID: job.name, + Scope: [ + job.namespace, + job.id, + ] + })); + + const transformedNodes = matchedNodes.models.map(node => ({ + ID: node.name, + Scope: [ + node.id, + ], + })); + + const truncatedGroups = transformedGroups.slice(0, 20); + const truncatedJobs = transformedJobs.slice(0, 20); + const truncatedNodes = transformedNodes.slice(0, 20); + return { Matches: { - groups: matchedGroups.models.map(group => ({ - ID: group.name, - Scope: [ - group.job.namespace, - group.job.id, - ], - })), - jobs: matchedJobs.models.map(job => ({ - ID: job.name, - Scope: [ - job.namespace, - job.id, - ] - })), - nodes: matchedNodes.models.map(node => ({ - ID: node.name, - Scope: [ - node.id, - ], - })) - } + groups: truncatedGroups, + jobs: truncatedJobs, + nodes: truncatedNodes, + }, + Truncations: { + groups: truncatedGroups.length < transformedGroups.length, + jobs: truncatedJobs.length < transformedJobs.length, + nodes: truncatedNodes.length < transformedNodes.length, + }, } }); diff --git a/ui/tests/acceptance/search-test.js b/ui/tests/acceptance/search-test.js index a9c8cd595d9..341d172c752 100644 --- a/ui/tests/acceptance/search-test.js +++ b/ui/tests/acceptance/search-test.js @@ -185,6 +185,24 @@ module('Acceptance | search', function(hooks) { }); }); + test('server-side truncation is indicated in the group label', async function(assert) { + server.create('node', { name: 'xyz' }); + + for (let i = 0; i < 21; i++) { + server.create('job', { id: `job-${i}`, namespaceId: 'default' }); + } + + await visit('/'); + + await selectSearch(Layout.navbar.search.scope, 'job'); + + Layout.navbar.search.as(search => { + search.groups[0].as(jobs => { + assert.equal(jobs.name, 'Jobs (showing 10 of 20+)'); + }); + }); + }); + skip('search highlights matching substrings', async function(assert) { server.create('node', { name: 'xyz' }); From 28657737a177f0ac9455687555552979724adeac Mon Sep 17 00:00:00 2001 From: Buck Doyle Date: Wed, 21 Apr 2021 10:14:36 -0500 Subject: [PATCH 10/25] Add fuzzy search feature detection --- ui/app/components/global-header.js | 1 + ui/app/routes/application.js | 5 +++++ ui/app/services/system.js | 18 ++++++++++++++++++ ui/app/templates/components/global-header.hbs | 8 +++++--- ui/tests/acceptance/search-test.js | 13 ++++++++++++- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/ui/app/components/global-header.js b/ui/app/components/global-header.js index a1b13deb1a2..2e89fcb5518 100644 --- a/ui/app/components/global-header.js +++ b/ui/app/components/global-header.js @@ -5,6 +5,7 @@ import { inject as service } from '@ember/service'; @classic export default class GlobalHeader extends Component { @service config; + @service system; 'data-test-global-header' = true; onHamburgerClick() {} diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index a52f185c7fc..4059b6d924a 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -48,11 +48,16 @@ export default class ApplicationRoute extends Route { .perform() .catch(); + const checkFuzzySearchPresence = this.get('system.checkFuzzySearchPresence') + .perform() + .catch(); + const promises = await RSVP.all([ this.get('system.regions'), this.get('system.defaultRegion'), fetchLicense, fetchSelfTokenAndPolicies, + checkFuzzySearchPresence, ]); if (!this.get('system.shouldShowRegions')) return promises; diff --git a/ui/app/services/system.js b/ui/app/services/system.js index 0f526b57175..bdbc748956f 100644 --- a/ui/app/services/system.js +++ b/ui/app/services/system.js @@ -157,7 +157,25 @@ export default class SystemService extends Service { }) fetchLicense; + @task(function*() { + try { + const request = yield this.token.authorizedRequest('/v1/search/fuzzy', { + method: 'POST', + body: JSON.stringify({ + Text: 'PLACEHOLDERFIXME', + Context: 'jobs', + }), + }); + + return request.ok; + } catch (e) { + return false; + } + }) + checkFuzzySearchPresence; + @alias('fetchLicense.lastSuccessful.value') license; + @alias('checkFuzzySearchPresence.last.value') fuzzySearchEnabled; @computed('license.License.Features.[]') get features() { diff --git a/ui/app/templates/components/global-header.hbs b/ui/app/templates/components/global-header.hbs index 4beb56b39aa..2fd688ee6c1 100644 --- a/ui/app/templates/components/global-header.hbs +++ b/ui/app/templates/components/global-header.hbs @@ -7,9 +7,11 @@ - {{#unless (media "isMobile")}} - - {{/unless}} + {{#if this.system.fuzzySearchEnabled}} + {{#unless (media "isMobile")}} + + {{/unless}} + {{/if}}