Appmanager: implement functional search bar
            reworked handlers for different app types

Change-Id: I82d3c856aa5c583dcdcf83ed6fbaf440bc4c8f87
diff --git a/core/installer/app_repository.go b/core/installer/app_repository.go
index 1befd7c..381a84f 100644
--- a/core/installer/app_repository.go
+++ b/core/installer/app_repository.go
@@ -9,6 +9,7 @@
 	"io"
 	"log"
 	"net/http"
+	"strings"
 
 	"github.com/go-git/go-billy/v5"
 	"sigs.k8s.io/yaml"
@@ -71,6 +72,7 @@
 type AppRepository interface {
 	GetAll() ([]App, error)
 	Find(name string) (App, error)
+	Filter(query string) ([]App, error)
 }
 
 type InMemoryAppRepository struct {
@@ -104,6 +106,19 @@
 	)
 }
 
+func (r InMemoryAppRepository) Filter(query string) ([]App, error) {
+	var filteredApps []App
+	if query == "" {
+		return r.GetAll()
+	}
+	for _, a := range r.apps {
+		if strings.Contains(strings.ToLower(a.Name()), strings.ToLower(query)) {
+			filteredApps = append(filteredApps, a)
+		}
+	}
+	return filteredApps, nil
+}
+
 func CreateStoreApps() []App {
 	return CreateEnvApps(storeEnvAppConfigs)
 }
diff --git a/core/installer/welcome/appmanager-tmpl/base.html b/core/installer/welcome/appmanager-tmpl/base.html
index fa77d97..dd0549a 100644
--- a/core/installer/welcome/appmanager-tmpl/base.html
+++ b/core/installer/welcome/appmanager-tmpl/base.html
@@ -14,9 +14,9 @@
           <aside id="menu-nav">
             <nav id="menu" class="is-sticky-above-lg">
                 <ul>
-                  <li><a href="/" class="{{ if (eq .CurrentPage "ALL") }}primary{{ end }}">All</a></li>
-                  <li><a href="/installed" class="{{ if (eq .CurrentPage "INSTALLED") }}primary{{ end }}">Installed</a></li>
-                  <li><a href="/not-installed" class="{{ if (eq .CurrentPage "NOT_INSTALLED") }}primary{{ end }}">Not Installed</a></li>
+                  <li><a href="/" class="{{ if (eq .CurrentPage "all") }}primary{{ end }}">All</a></li>
+                  <li><a href="/installed" class="{{ if (eq .CurrentPage "installed") }}primary{{ end }}">Installed</a></li>
+                  <li><a href="/not-installed" class="{{ if (eq .CurrentPage "not-installed") }}primary{{ end }}">Not Installed</a></li>
                   <hr>
                   {{ block "extra_menu" . }}{{ end }}
                 </ul>
@@ -26,6 +26,6 @@
 			  {{ block "content" . }}{{ end }}
 		  </div>
       </main>
-    <script src="/static/app-manager.js?v=0.0.2"></script>
+    <script src="/static/app-manager.js?v=0.0.10"></script>
 	</body>
 </html>
diff --git a/core/installer/welcome/appmanager-tmpl/index.html b/core/installer/welcome/appmanager-tmpl/index.html
index 6ba92c3..fcb613f 100644
--- a/core/installer/welcome/appmanager-tmpl/index.html
+++ b/core/installer/welcome/appmanager-tmpl/index.html
@@ -1,13 +1,14 @@
 {{ define "header" }}
-  <form class="search-bar">
-      <input name="search" type="search" placeholder="Search" />
+  <form id="search-form" class="search-bar" method="GET" action="/{{ .SearchTarget }}">
+      <input id="search-input" name="query" type="search" placeholder="Search" value="{{ .SearchValue }}"/>
   </form>
+  <input type="hidden" id="page-type" value="{{ .SearchTarget }}" />
 {{ end }}
 
 {{ define "content" }}
 <aside>
     <nav>
-        <ul>
+        <ul id="app-list">
             {{ range .Apps }}
                 <li class="app-card">
                     <a href="/app/{{ .Slug }}" class="app-link">
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index 2c434c6..29d19ed 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -100,11 +100,10 @@
 	r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
 	r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
 	r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
-	r.HandleFunc("/", s.handleIndex).Methods(http.MethodGet)
-	r.HandleFunc("/not-installed", s.handleNotInstalledApps).Methods(http.MethodGet)
-	r.HandleFunc("/installed", s.handleInstalledApps).Methods(http.MethodGet)
 	r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
 	r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
+	r.HandleFunc("/{pageType}", s.handleAppsList).Methods(http.MethodGet)
+	r.HandleFunc("/", s.handleAppsList).Methods(http.MethodGet)
 	fmt.Printf("Starting HTTP server on port: %d\n", s.port)
 	return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
 }
@@ -317,88 +316,51 @@
 }
 
 type PageData struct {
-	Apps        []app
-	CurrentPage string
+	Apps         []app
+	CurrentPage  string
+	SearchTarget string
+	SearchValue  string
 }
 
-func (s *AppManagerServer) handleIndex(w http.ResponseWriter, r *http.Request) {
-	all, err := s.r.GetAll()
-	if err != nil {
-		log.Printf("all apps: %v", err)
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
+func (s *AppManagerServer) handleAppsList(w http.ResponseWriter, r *http.Request) {
+	pageType := mux.Vars(r)["pageType"]
+	if pageType == "" {
+		pageType = "all"
 	}
-	resp := make([]app, 0)
-	for _, a := range all {
-		instances, err := s.m.FindAllAppInstances(a.Slug())
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
-	}
-	data := PageData{
-		Apps:        resp,
-		CurrentPage: "ALL",
-	}
-	if err := s.tmpl.index.Execute(w, data); err != nil {
-		log.Printf("executing template: %v", err)
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-}
-
-func (s *AppManagerServer) handleNotInstalledApps(w http.ResponseWriter, r *http.Request) {
-	all, err := s.r.GetAll()
+	searchQuery := r.FormValue("query")
+	apps, err := s.r.Filter(searchQuery)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 	resp := make([]app, 0)
-	for _, a := range all {
+	for _, a := range apps {
 		instances, err := s.m.FindAllAppInstances(a.Slug())
 		if err != nil {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
-		if len(instances) == 0 {
-			resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
-		}
-	}
-	data := PageData{
-		Apps:        resp,
-		CurrentPage: "NOT_INSTALLED",
-	}
-	if err := s.tmpl.index.Execute(w, data); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-}
-
-func (s *AppManagerServer) handleInstalledApps(w http.ResponseWriter, r *http.Request) {
-	all, err := s.r.GetAll()
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	resp := make([]app, 0)
-	for _, a := range all {
-		instances, err := s.m.FindAllAppInstances(a.Slug())
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		if len(instances) != 0 {
+		switch pageType {
+		case "installed":
+			if len(instances) != 0 {
+				resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
+			}
+		case "not-installed":
+			if len(instances) == 0 {
+				resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
+			}
+		default:
 			resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
 		}
 	}
 	data := PageData{
-		Apps:        resp,
-		CurrentPage: "INSTALLED",
+		Apps:         resp,
+		CurrentPage:  pageType,
+		SearchTarget: pageType,
+		SearchValue:  searchQuery,
 	}
 	if err := s.tmpl.index.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
 	}
 }
 
diff --git a/core/installer/welcome/static/app-manager.js b/core/installer/welcome/static/app-manager.js
index 5cd568b..55357f3 100644
--- a/core/installer/welcome/static/app-manager.js
+++ b/core/installer/welcome/static/app-manager.js
@@ -1,4 +1,13 @@
+function delaySearch(func, wait) {
+    let timeout;
+    return function (...args) {
+        clearTimeout(timeout);
+        timeout = setTimeout(() => func.apply(this, args), wait);
+    };
+}
+
 document.addEventListener("DOMContentLoaded", function () {
+    let searchRequestCount = 0;
     const page = document.documentElement;
     const headerHeight = parseFloat(getComputedStyle(page).getPropertyValue('--pico-header-height').replace("px", ""));
     const nav = document.getElementById("menu");
@@ -7,6 +16,39 @@
     const menu = document.getElementById("menu-nav");
     const menuHeight = parseFloat(getComputedStyle(document.getElementById('menu-nav')).height.replace("px", "")) + 15;
     menu.style.setProperty("height", `${menuHeight}px`);
+    const searchForm = document.getElementById('search-form');
+    const searchInput = document.getElementById('search-input');
+    function fetchAndUpdateAppList() {
+        searchRequestCount++;
+        const currentRequest = searchRequestCount;
+        const formData = new FormData(searchForm);
+        const query = formData.get('query');
+        const pageType = document.getElementById('page-type').value;
+        const url = `/${pageType}?query=${encodeURIComponent(query)}`;
+        fetch(url, {
+            method: 'GET'
+        })
+            .then(response => response.text())
+            .then(html => {
+                if (currentRequest !== searchRequestCount) {
+                    return;
+                }
+                const tempDiv = document.createElement('div');
+                tempDiv.innerHTML = html;
+                const newAppListHTML = tempDiv.querySelector('#app-list').innerHTML;
+                const appListContainer = document.getElementById("app-list");
+                appListContainer.innerHTML = newAppListHTML;
+            })
+            .catch(error => console.error('Error fetching app list:', error));
+    }
+    const delayedFetchAndUpdateAppList = delaySearch(fetchAndUpdateAppList, 300);
+    searchForm.addEventListener('submit', (event) => {
+        event.preventDefault();
+        fetchAndUpdateAppList();
+    });
+    searchInput.addEventListener('input', () => {
+        delayedFetchAndUpdateAppList();
+    });
 });
 
 let prevWindowHeight = window.innerHeight;