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