← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~jonesogolo/maas-site-manager:1390-create-search-filter-component into maas-site-manager:main

 

Jones Ogolo has proposed merging ~jonesogolo/maas-site-manager:1390-create-search-filter-component into maas-site-manager:main.

Commit message:
Create request for search and filtering

Requested reviews:
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~jonesogolo/maas-site-manager/+git/maas-site-manager/+merge/438670

Added Request for filtering
Created custom param serializer to account for duplicate search keys
-- 
Your team MAAS Committers is requested to review the proposed merge of ~jonesogolo/maas-site-manager:1390-create-search-filter-component into maas-site-manager:main.
diff --git a/frontend/src/api/handlers.ts b/frontend/src/api/handlers.ts
index 9eff927..0c9ecdd 100644
--- a/frontend/src/api/handlers.ts
+++ b/frontend/src/api/handlers.ts
@@ -1,3 +1,5 @@
+import { customParamSerializer } from "../utils";
+
 import api from "./api";
 import urls from "./urls";
 
@@ -15,3 +17,18 @@ export const getSites = async (params: GetSitesQueryParams) => {
     console.error(error);
   }
 };
+
+export const getFilteredSites = async (params: GetSitesQueryParams, queryText: string) => {
+  try {
+    const response = await api.get(urls.sites, {
+      params,
+      paramsSerializer: {
+        serialize: (params) => customParamSerializer(params, queryText),
+      },
+    });
+    return response.data;
+  } catch (error) {
+    // eslint-disable-next-line no-console
+    console.error(error);
+  }
+};
diff --git a/frontend/src/components/SitesList/SitesList.tsx b/frontend/src/components/SitesList/SitesList.tsx
index 1b520ea..fcaa759 100644
--- a/frontend/src/components/SitesList/SitesList.tsx
+++ b/frontend/src/components/SitesList/SitesList.tsx
@@ -2,7 +2,8 @@ import { useState } from "react";
 
 import { Pagination } from "@canonical/react-components";
 
-import { useSitesQuery } from "../../hooks/api";
+import { useSitesQuery, useSitesFilterQuery } from "../../hooks/api";
+import { parseSearchTextToQueryParams } from "../../utils";
 
 import SitesTable from "./components/SitesTable";
 
@@ -11,19 +12,33 @@ const DEFAULT_PAGE_SIZE = 50;
 const SitesList = () => {
   const [page, setPage] = useState(0);
   const [size] = useState(DEFAULT_PAGE_SIZE);
+  const [searchText, setSearchText] = useState("");
   const { data, isLoading, isFetchedAfterMount } = useSitesQuery({ page: `${page}`, size: `${size}` });
+  const { data: searchData, isLoading: searchLoading } = useSitesFilterQuery(
+    { page: `${page}`, size: `${size}` },
+    parseSearchTextToQueryParams(searchText),
+  );
+
+  const isDataLoading = isLoading || searchLoading;
+  // Prioritize search results if any
+  const tableData = searchText ? searchData : data;
 
   return (
     <div>
-      <SitesTable data={data} isFetchedAfterMount={isFetchedAfterMount} isLoading={isLoading} />
+      <SitesTable
+        data={tableData}
+        isFetchedAfterMount={isFetchedAfterMount}
+        isLoading={isDataLoading}
+        setSearchText={setSearchText}
+      />
       <Pagination
         currentPage={page + 1}
-        disabled={isLoading}
+        disabled={isDataLoading}
         itemsPerPage={size}
         paginate={(page) => {
           setPage(page - 1);
         }}
-        totalItems={data?.total || 0}
+        totalItems={tableData?.total || 0}
       />
     </div>
   );
diff --git a/frontend/src/components/SitesList/components/SitesTable.tsx b/frontend/src/components/SitesList/components/SitesTable.tsx
index ebaf1bb..800c2c2 100644
--- a/frontend/src/components/SitesList/components/SitesTable.tsx
+++ b/frontend/src/components/SitesList/components/SitesTable.tsx
@@ -34,7 +34,10 @@ const SitesTable = ({
   data,
   isFetchedAfterMount,
   isLoading,
-}: Pick<UseSitesQueryResult, "data" | "isLoading" | "isFetchedAfterMount">) => {
+  setSearchText,
+}: Pick<UseSitesQueryResult, "data" | "isLoading" | "isFetchedAfterMount"> & {
+  setSearchText: (text: string) => void;
+}) => {
   const [columnVisibility, setColumnVisibility] = useLocalStorageState("sitesTableColumnVisibility", {
     defaultValue: {},
   });
@@ -194,7 +197,12 @@ const SitesTable = ({
 
   return (
     <>
-      <SitesTableControls allColumns={table.getAllLeafColumns()} data={data} isLoading={isLoading} />
+      <SitesTableControls
+        allColumns={table.getAllLeafColumns()}
+        data={data}
+        isLoading={isLoading}
+        setSearchText={setSearchText}
+      />
       <table aria-label="sites" className="sites-table">
         <thead>
           {table.getHeaderGroups().map((headerGroup) => (
diff --git a/frontend/src/components/SitesList/components/SitesTableControls.tsx b/frontend/src/components/SitesList/components/SitesTableControls.tsx
index 255b698..7204b02 100644
--- a/frontend/src/components/SitesList/components/SitesTableControls.tsx
+++ b/frontend/src/components/SitesList/components/SitesTableControls.tsx
@@ -1,5 +1,3 @@
-import { useState } from "react";
-
 import { Row, Col, SearchBox } from "@canonical/react-components";
 
 import type { UseSitesQueryResult } from "../../../hooks/api";
@@ -12,8 +10,11 @@ const SitesTableControls = ({
   data,
   isLoading,
   allColumns,
-}: { allColumns: SitesColumn[] } & Pick<UseSitesQueryResult, "data" | "isLoading">) => {
-  const [searchText, setSearchText] = useState("");
+  setSearchText,
+}: { allColumns: SitesColumn[]; setSearchText: (text: string) => void } & Pick<
+  UseSitesQueryResult,
+  "data" | "isLoading"
+>) => {
   const handleSearchInput = (inputValue: string) => {
     setSearchText(inputValue);
   };
@@ -26,12 +27,7 @@ const SitesTableControls = ({
         </h2>
       </Col>
       <Col size={8}>
-        <SearchBox
-          externallyControlled
-          onChange={handleSearchInput}
-          placeholder="Search and filter"
-          value={searchText}
-        />
+        <SearchBox externallyControlled onChange={handleSearchInput} placeholder="Search and filter" />
       </Col>
       <Col className="u-flex u-flex--align-end u-flex--column" size={2}>
         <ColumnsVisibilityControl columns={allColumns} />
diff --git a/frontend/src/hooks/api.ts b/frontend/src/hooks/api.ts
index ccf0b4d..9ea4707 100644
--- a/frontend/src/hooks/api.ts
+++ b/frontend/src/hooks/api.ts
@@ -1,7 +1,7 @@
 import { useQuery } from "@tanstack/react-query";
 
 import type { GetSitesQueryParams } from "../api/handlers";
-import { getSites } from "../api/handlers";
+import { getSites, getFilteredSites } from "../api/handlers";
 import type { SitesQueryResult } from "../api/types";
 
 export type UseSitesQueryResult = ReturnType<typeof useSitesQuery>;
@@ -12,3 +12,13 @@ export const useSitesQuery = ({ page, size }: GetSitesQueryParams) =>
     queryFn: () => getSites({ page, size }),
     keepPreviousData: true,
   });
+
+export type UseSitesFilterQueryResult = ReturnType<typeof useSitesFilterQuery>;
+
+export const useSitesFilterQuery = ({ page, size }: GetSitesQueryParams, queryText: string) =>
+  useQuery<SitesQueryResult>({
+    queryKey: ["sites", page, size, queryText],
+    queryFn: () => getFilteredSites({ page, size }, queryText),
+    keepPreviousData: true,
+    enabled: !!queryText,
+  });
diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts
index 4849540..0e98457 100644
--- a/frontend/src/utils.ts
+++ b/frontend/src/utils.ts
@@ -8,3 +8,21 @@ if (typeof window !== "undefined") {
 export const getCountryName = (countryCode: string) => {
   return getName(countryCode, "en", { select: "official" });
 };
+
+export const parseSearchTextToQueryParams = (text: string) => {
+  // if just text => q=text
+  // if text:result => text=result
+  if (!text) return "";
+  return text
+    .split(" ")
+    .map((item) => (item.includes(":") ? item.replaceAll(":", "=") : `q=${item}`))
+    .join("&");
+};
+
+export const customParamSerializer = (params: Record<string, string>, queryText?: string) => {
+  return (
+    Object.entries(Object.assign({}, params))
+      .map(([key, value]) => `${key}=${value}`)
+      .join("&") + `${queryText ? "&" + queryText : ""}`
+  );
+};

Follow ups