From 7c9d30f382eb180e568b85fdd2b3e5cc8d29420e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tina=20M=C3=BCller?= <tina.mueller@suse.com>
Date: Tue, 7 Nov 2023 17:12:53 +0100
Subject: [PATCH] Add headers to search results

Now one can easily jump to the results by type and see the individual number
of results

Slightly related issue: https://progress.opensuse.org/issues/137243
---
 assets/javascripts/openqa.js                  | 53 ++++++++++++-------
 lib/OpenQA/WebAPI/Controller/API/V1/Search.pm | 23 ++++----
 t/api/15-search.t                             | 25 +++++----
 t/ui/15-search.t                              |  5 +-
 templates/webapi/search/search.html.ep        |  6 +--
 5 files changed, 69 insertions(+), 43 deletions(-)

diff --git a/assets/javascripts/openqa.js b/assets/javascripts/openqa.js
index 635649c5f181..4d5b11994b0d 100644
--- a/assets/javascripts/openqa.js
+++ b/assets/javascripts/openqa.js
@@ -290,28 +290,45 @@ function renderSearchResults(query, url) {
     }
     spinner.style.display = 'none';
     var heading = document.getElementById('results-heading');
-    heading.appendChild(document.createTextNode(': ' + json.data.length + ' matches found'));
+    heading.appendChild(document.createTextNode(': ' + json.data.total_count + ' matches found'));
     var results = document.createElement('div');
     results.id = 'results';
     results.className = 'list-group';
-    json.data.forEach(function (value, index) {
-      var item = document.createElement('div');
-      item.className = 'list-group-item';
-      var header = document.createElement('div');
-      header.className = 'd-flex w-100 justify-content-between';
-      var title = document.createElement('h5');
-      title.className = 'occurrence mb-1';
-      title.appendChild(document.createTextNode(value.occurrence));
-      header.appendChild(title);
-      item.appendChild(header);
-      if (value.contents) {
-        var contents = document.createElement('pre');
-        contents.className = 'contents mb-1';
-        contents.appendChild(document.createTextNode(value.contents));
-        item.appendChild(contents);
+    const types = ['code', 'modules', 'templates'];
+
+    for (var i = 0; i < types.length; i++) {
+      var searchtype = types[i];
+      var searchresults = json.data.results[searchtype];
+      if (searchresults.length > 0) {
+        var header = document.createElement('div');
+        header.id = searchtype;
+        var bold = document.createElement('strong');
+        var textnode = document.createTextNode(searchtype + ': ' + searchresults.length);
+        bold.appendChild(textnode);
+
+        header.className = 'list-group-item';
+        header.appendChild(bold);
+        results.append(header);
       }
-      results.append(item);
-    });
+      searchresults.forEach(function (value, index) {
+        var item = document.createElement('div');
+        item.className = 'list-group-item';
+        var header = document.createElement('div');
+        header.className = 'd-flex w-100 justify-content-between';
+        var title = document.createElement('h5');
+        title.className = 'occurrence mb-1';
+        title.appendChild(document.createTextNode(value.occurrence));
+        header.appendChild(title);
+        item.appendChild(header);
+        if (value.contents) {
+          var contents = document.createElement('pre');
+          contents.className = 'contents mb-1';
+          contents.appendChild(document.createTextNode(value.contents));
+          item.appendChild(contents);
+        }
+        results.append(item);
+      });
+    }
     const oldResults = document.getElementById('results');
     oldResults.parentElement.replaceChild(results, oldResults);
   };
diff --git a/lib/OpenQA/WebAPI/Controller/API/V1/Search.pm b/lib/OpenQA/WebAPI/Controller/API/V1/Search.pm
index 1df04a48775d..14ac66dd09b7 100644
--- a/lib/OpenQA/WebAPI/Controller/API/V1/Search.pm
+++ b/lib/OpenQA/WebAPI/Controller/API/V1/Search.pm
@@ -176,22 +176,27 @@ sub query {
       unless $self->app->minion->lock($lockname, 60, {limit => $self->app->config->{'rate_limits'}->{'search'}});
 
     my $cap = $self->app->config->{'global'}->{'search_results_limit'};
-    my @results;
+    my %results;
     my $keywords = $validation->param('q');
 
+    my %json = (total_count => 0, results => \%results);
     my $perl_module_results = $self->_search_perl_modules($keywords, $cap);
-    $cap -= scalar @{$perl_module_results};
-    push @results, @{$perl_module_results};
-    return $self->render(json => {data => \@results}) unless $cap > 0;
+    $json{total_count} += @{$perl_module_results};
+    $cap -= @{$perl_module_results};
+    $results{code} = $perl_module_results;
+    return $self->render(json => {data => \%json}) unless $cap > 0;
 
     my $job_module_results = $self->_search_job_modules($keywords, $cap);
-    $cap -= scalar @{$job_module_results};
-    push @results, @{$job_module_results};
-    return $self->render(json => {data => \@results}) unless $cap > 0;
+    $json{total_count} += @{$job_module_results};
+    $cap -= @{$job_module_results};
+    $results{modules} = $job_module_results;
+    return $self->render(json => {data => \%json}) unless $cap > 0;
 
-    push @results, @{$self->_search_job_templates($keywords, $cap)};
+    my $job_template_resuts = $self->_search_job_templates($keywords, $cap);
+    $json{total_count} += @{$job_template_resuts};
+    $results{templates} = $job_template_resuts;
 
-    $self->render(json => {data => \@results});
+    $self->render(json => {data => \%json});
 }
 
 1;
diff --git a/t/api/15-search.t b/t/api/15-search.t
index 582cee69deb0..a644d8a75e4c 100644
--- a/t/api/15-search.t
+++ b/t/api/15-search.t
@@ -17,9 +17,12 @@ $t->app->config->{rate_limits}->{search} = 10;
 subtest 'Perl modules' => sub {
     $t->get_ok('/api/v1/experimental/search?q=timezone', 'search successful');
     $t->json_is('/error' => undef, 'no errors');
-    $t->json_is('/data/0' => {occurrence => 'opensuse/tests/installation/installer_timezone.pm'}, 'module found');
     $t->json_is(
-        '/data/1' => {
+        '/data/results/code/0' => {occurrence => 'opensuse/tests/installation/installer_timezone.pm'},
+        'module found'
+    );
+    $t->json_is(
+        '/data/results/code/1' => {
             occurrence => 'opensuse/tests/installation/installer_timezone.pm',
             contents => qq{    3 # Summary: Verify timezone settings page\n}
               . qq{   10     assert_screen "inst-timezone", 125 || die 'no timezone';}
@@ -31,9 +34,9 @@ subtest 'Perl modules' => sub {
 subtest 'Python modules' => sub {
     $t->get_ok('/api/v1/experimental/search?q=search', 'search successful');
     $t->json_is('/error' => undef, 'no errors');
-    $t->json_is('/data/0' => {occurrence => 'opensuse/tests/openQA/search.py'}, 'module found');
+    $t->json_is('/data/results/code/0' => {occurrence => 'opensuse/tests/openQA/search.py'}, 'module found');
     $t->json_is(
-        '/data/1' => {
+        '/data/results/code/1' => {
             occurrence => 'opensuse/tests/openQA/search.py',
             contents => qq{    6     assert_and_click('openqa-search')\n}
               . qq{    9     assert_screen('openqa-search-results')}
@@ -64,14 +67,14 @@ subtest 'Job modules' => sub {
     $t->get_ok('/api/v1/experimental/search?q=ipsum', 'search successful');
     $t->json_is('/error' => undef, 'no errors');
     $t->json_is(
-        '/data/0' => {
+        '/data/results/modules/0' => {
             occurrence => 'lorem',
             contents => "tests/lorem/ipsum.pm\n" . "tests/lorem/ipsum_dolor.py"
         },
         'job module found'
     );
     $t->json_is(
-        '/data/1' => undef,
+        '/data/total_count' => 1,
         'no additional job module found'
     );
 };
@@ -97,11 +100,11 @@ subtest 'Job templates' => sub {
     $t->get_ok('/api/v1/experimental/search?q=fancy', 'search successful');
     $t->json_is('/error' => undef, 'no errors');
     $t->json_is(
-        '/data/0' => {occurrence => 'Cool Group', contents => "fancy-example\nVery posh"},
+        '/data/results/templates/0' => {occurrence => 'Cool Group', contents => "fancy-example\nVery posh"},
         'job template found'
     );
     $t->json_is(
-        '/data/1' => undef,
+        '/data/total_count' => 1,
         'no additional job template found'
     );
 
@@ -116,7 +119,7 @@ subtest 'Job templates' => sub {
     $t->get_ok('/api/v1/experimental/search?q=apple', 'search successful');
     $t->json_is('/error' => undef, 'no errors');
     $t->json_is(
-        '/data/0' => {occurrence => 'Cool Group', contents => "apple\n"},
+        '/data/results/templates/0' => {occurrence => 'Cool Group', contents => "apple\n"},
         'job template was found by using test suite name'
     );
 };
@@ -124,7 +127,7 @@ subtest 'Job templates' => sub {
 subtest 'Limits' => sub {
     $t->app->config->{global}->{search_results_limit} = 1;
     $t->get_ok('/api/v1/experimental/search?q=test', 'Extensive search with limit')->status_is(200);
-    $t->json_is('/data/1' => undef, 'capped at one match');
+    $t->json_is('/data/results/templates/1' => undef, 'capped at one match');
 };
 
 subtest 'Errors' => sub {
@@ -133,7 +136,7 @@ subtest 'Errors' => sub {
 
     $t->get_ok('/api/v1/experimental/search?q=*', 'wildcard is interpreted literally');
     $t->json_is(
-        '/data/0' => {
+        '/data/results/code/0' => {
             occurrence => "opensuse\/tests\/openQA\/search.py",
             contents => "    1 from testapi import *",
         },
diff --git a/t/ui/15-search.t b/t/ui/15-search.t
index f943777d7546..a2a44035dc45 100644
--- a/t/ui/15-search.t
+++ b/t/ui/15-search.t
@@ -36,9 +36,10 @@ subtest 'Perl modules' => sub {
     my $header = $driver->find_element_by_id('results-heading');
     my $results = $driver->find_element_by_id('results');
     my @entries = $results->children('.list-group-item');
-    is $header->get_text(), 'Search results: ' . scalar @entries . ' matches found', 'number of results in header';
-    is scalar @entries, 2, '2 elements' or return;
+    is $header->get_text(), 'Search results: ' . (@entries - 1) . ' matches found', 'number of results in header';
+    is scalar @entries, 3, '3 elements' or return;
 
+    shift @entries;
     my $first = $entries[0];
     is $first->child('.occurrence')->get_text(), 'opensuse/tests/installation/installer_timezone.pm',
       'expected occurrence';
diff --git a/templates/webapi/search/search.html.ep b/templates/webapi/search/search.html.ep
index 51f8c6cbc34e..751a253e2e06 100644
--- a/templates/webapi/search/search.html.ep
+++ b/templates/webapi/search/search.html.ep
@@ -7,9 +7,9 @@
 
 <div>
     <h2 id="results-heading">Search results</h2>
-    <p>The search currently finds <b>job templates</b> by name or description,
-       <b>job modules</b> by filename,
-       or test modules within the test distributions,
+    <p>The search currently finds <b><a href="#templates">job templates</a></b> by name or description,
+       <b><a href="#modules">job modules</a></b> by filename,
+       or <a href="#code">test modules</a> within the test distributions,
        either by <b>filename</b> or <b>source code</b>.</p>
     <div id="flash-messages"></div>
     <p id="progress-indication" style="display: none">