From 8bea19a562a3b5b124f32b60494338a2e466f346 Mon Sep 17 00:00:00 2001
From: Anna Finn <afinn12@bu.edu>
Date: Thu, 7 Nov 2024 12:31:06 -0500
Subject: [PATCH] dashboard: Add search box and link to URL

Adds an input form for searching job names. Searches are appended to the URL.

Fixes #4

Signed-off-by: Anna Finn <afinn12@bu.edu>
---
 components/searchForm.js |  22 +++++++
 pages/index.js           | 128 +++++++++++++++++++++++++++++++++++----
 2 files changed, 138 insertions(+), 12 deletions(-)
 create mode 100644 components/searchForm.js

diff --git a/components/searchForm.js b/components/searchForm.js
new file mode 100644
index 0000000..63f5551
--- /dev/null
+++ b/components/searchForm.js
@@ -0,0 +1,22 @@
+export const SearchForm = ({ handleSearch }) => {
+    return (
+        <div className="flex flex-col items-center md:text-base text-xs">
+            <div className="flex min-[1126px]:justify-end justify-center w-full"> 
+            <form className="p-2 bg-gray-700 rounded-md flex flex-row" onSubmit={(e) => handleSearch(e)}> 
+                <div>
+                    <label className="block text-white">Match Mode:</label>
+                    <select name="matchMode" className="px-1 h-fit rounded-lg">
+                        <option value="or">Match Any</option>
+                        <option value="and">Match All</option>
+                    </select>
+                </div>
+                <div className="mx-2">
+                    <label className="block text-white">Search Text:</label>
+                    <input type="text" name="value" required></input>
+                </div>
+                <button type="submit" className="bg-blue-500 text-white px-4 rounded-3xl">Submit</button>
+            </form>
+            </div>
+        </div>
+    );
+};
\ No newline at end of file
diff --git a/pages/index.js b/pages/index.js
index b12d28a..d7a210d 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -2,20 +2,23 @@ import { useEffect, useState } from "react";
 import { DataTable } from "primereact/datatable";
 import { Column } from "primereact/column";
 import { weatherTemplate, getWeatherIndex } from "../components/weatherTemplate";
+import { basePath } from "../next.config.js";
+import { SearchForm } from "../components/searchForm";
 
 
 export default function Home() {
-  const [loading, setLoading] = useState(true);
-  const [jobs, setJobs] = useState([]);
-  const [rows, setRows] = useState([]);
-  const [expandedRows, setExpandedRows] = useState([]);
+  const [loading,   setLoading]          = useState(true);
+  const [jobs,          setJobs]         = useState([]);
+  const [rows,          setRows]         = useState([]);
+  const [expandedRows,  setExpandedRows] = useState([]);
+  const [keepSearch,    setKeepSearch]   = useState(true);
 
   useEffect(() => {
     const fetchData = async () => {
       let data = {};
 
       if (process.env.NODE_ENV === "development") {
-        data = (await import("../job_stats.json")).default;
+        data = (await import("../localData/job_stats.json")).default;
       } else {
         const response = await fetch(
           "https://raw.githubusercontent.com/kata-containers/kata-containers.github.io" +
@@ -41,15 +44,59 @@ export default function Home() {
     fetchData();
   }, []);
 
+  // Filters the jobs s.t. all values must be contained in the name.
+  const matchAll = (filteredJobs, values) => {
+    return filteredJobs.filter((job) => {
+        const jobName = job.name.toLowerCase();
+        return values.every((val) => {
+            const decodedValue = decodeURIComponent(val).toLowerCase();
+            return jobName.includes(decodedValue);
+        });
+    });
+  };
+
+  // Filters the jobs s.t. at least one value must be contained in the name.
+  const matchAny = (filteredJobs, values) => {
+    return filteredJobs.filter((job) => {
+        const jobName = job.name.toLowerCase();
+        return values.some((val) => {
+            const decodedValue = decodeURIComponent(val).toLowerCase();
+            return jobName.includes(decodedValue); 
+        });
+    });
+  };
+
+
   useEffect(() => {
     setLoading(true);
+    let filteredJobs = jobs;
+
+    //Filter based on the URL. 
+    const urlParams = new URLSearchParams(window.location.search);
+    switch(urlParams.get("matchMode")) {
+      case "and":
+        filteredJobs = matchAll(filteredJobs, urlParams.getAll("value"));
+        break;
+      case "or":
+        filteredJobs = matchAny(filteredJobs, urlParams.getAll("value"));
+        break;
+      default:
+        break;
+    }
+
+
+    //Set the rows for the table.
+    setRows(
+      filteredJobs.map((job) => ({
+        name          : job.name,
+        runs          : job.runs,
+        fails         : job.fails,
+        skips         : job.skips,
+        required      : job.required,
+        weather       : getWeatherIndex(job),
+      }))
+    );
 
-    // Create rows to set into table.
-    const rows = jobs.map((job) => ({
-      ...job,
-      weather: getWeatherIndex(job),
-    }));
-    setRows(rows);
     setLoading(false);
   }, [jobs]);
 
@@ -66,6 +113,11 @@ export default function Home() {
     setExpandedRows(updatedExpandedRows);
   };
 
+  const buttonClass = (active) => `tab md:px-4 px-2 py-2 border-2
+    ${active ? "border-blue-500 bg-blue-500 text-white"
+      : "border-gray-300 bg-white hover:bg-gray-100"}`;
+
+
   // Template for rendering the Name column as a clickable item
   const nameTemplate = (rowData) => {
     return (
@@ -120,6 +172,39 @@ export default function Home() {
     );
   };
 
+    // Apply search terms to the URL and reload the page.
+    const handleSearch= (e) => {
+      // Prevent the default behavior so that we can keep search terms.
+      e.preventDefault();
+      const matchMode = e.target.matchMode.value;
+      const value = e.target.value.value.trimEnd();
+      if (value) {
+        // Append the new matchMode regardless of if search terms were kept.
+        const path = new URLSearchParams();
+        path.append("matchMode", matchMode);
+        if (keepSearch) {
+          // If keepSearch is true, add existing parameters in the URL.
+          const urlParams = new URLSearchParams(window.location.search);
+          urlParams.getAll("value").forEach((val) => {
+            path.append("value", val); 
+          });
+        }
+        //Add the search term from the form and redirect.
+        path.append("value", value);
+        window.location.assign(`${basePath}/?${path.toString()}`);
+      }
+    };
+  
+    // Clear the search parameters, but only if they exist.
+    const clearSearch = () => {
+      const urlParts = window.location.href.split("?");
+      if(urlParts[1] !== undefined){
+        window.location.assign(urlParts[0]);
+      }
+    }
+
+
+
   const renderTable = () => (
     <DataTable
       value={rows}
@@ -178,9 +263,28 @@ export default function Home() {
           "m-0 h-full p-4 overflow-x-hidden overflow-y-auto bg-surface-ground font-normal text-text-color antialiased select-text"
         }
       >
+        <div className="space-x-2 mx-auto">
+          <button
+            className={buttonClass()}
+            onClick={() => clearSearch()}>
+            Clear Search
+          </button>
+          <button
+            className={buttonClass(keepSearch)}
+            onClick={() => setKeepSearch(!keepSearch)}>
+            Keep URL Search Terms
+          </button>
+        </div>
+
+        <SearchForm handleSearch={handleSearch} />
+
+        <div className="mt-1 text-center md:text-lg text-base">
+          Total Rows: {rows.length}
+        </div>
+
         <div>{renderTable()}</div>
         <div className="mt-4 text-lg">Total Rows: {rows.length}</div>
       </main>
     </div>
   );
-}
+}
\ No newline at end of file