This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new 2ee5fe622 separate search into separate component to reduce re-renders
2ee5fe622 is described below

commit 2ee5fe622ab0e8acf9faab81ba0473b95f0d2865
Author: ganning127 <[email protected]>
AuthorDate: Sat Jul 19 11:00:08 2025 -0700

    separate search into separate component to reduce re-renders
---
 .../src/components/resources/ResourceFilters.tsx   | 364 +++++++++++++++++++++
 .../src/components/resources/index.tsx             | 360 ++------------------
 2 files changed, 390 insertions(+), 334 deletions(-)

diff --git 
a/airavata-research-portal/src/components/resources/ResourceFilters.tsx 
b/airavata-research-portal/src/components/resources/ResourceFilters.tsx
new file mode 100644
index 000000000..d5c97b03b
--- /dev/null
+++ b/airavata-research-portal/src/components/resources/ResourceFilters.tsx
@@ -0,0 +1,364 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import {useEffect, useState} from "react";
+import {Box, Button, Code, HStack, Input, Spinner, Text, VStack} from 
"@chakra-ui/react";
+import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum.ts";
+import {toaster} from "@/components/ui/toaster.tsx";
+import {useNavigate} from "react-router";
+import {Tag as TagEntity} from "@/interfaces/TagType.tsx";
+import api from "@/lib/api.ts";
+import {CONTROLLER} from "@/lib/controller.ts";
+import {resourceTypeToColor} from "@/lib/util.ts";
+import {FaCheck} from "react-icons/fa";
+import {Resource} from "@/interfaces/ResourceType.ts";
+
+const getResources = async (
+    types: ResourceTypeEnum[],
+    stringTagsArr: string[],
+    searchText: string
+) => {
+  const response = await api.get(`${CONTROLLER.resources}/public`, {
+    params: {
+      type: types.join(","),
+      tag: stringTagsArr.join(","),
+      nameSearch: searchText,
+      pageNumber: 0,
+      pageSize: 100,
+    },
+  });
+  return response.data;
+};
+
+const getTags = async () => {
+  try {
+    const response = await api.get(`${CONTROLLER.resources}/public/tags/all`);
+    return response.data;
+  } catch (error) {
+    console.error("Error fetching:", error);
+  }
+};
+
+export const ResourceFilters = ({setResources, resources}: {
+  setResources: (resources: Resource[]) => void,
+  resources: Resource[]
+}) => {
+  const [searchText, setSearchText] = useState("");
+  const [hydrated, setHydrated] = useState(false);
+  const [tags, setTags] = useState<string[]>([]);
+  const [suggestions, setSuggestions] = useState<string[]>([]);
+  const [resourceTypes, setResourceTypes] = useState<ResourceTypeEnum[]>([]);
+  const [loading, setLoading] = useState(false);
+
+  const navigate = useNavigate();
+
+  const labels = [
+    ResourceTypeEnum.REPOSITORY,
+    ResourceTypeEnum.NOTEBOOK,
+    ResourceTypeEnum.DATASET,
+    ResourceTypeEnum.MODEL,
+  ];
+
+  // Debounce the callback to the parent
+  useEffect(() => {
+    const handler = setTimeout(() => {
+      setSearchText(searchText);
+    }, 400);
+
+    return () => {
+      clearTimeout(handler);
+    };
+  }, [searchText, setSearchText]);
+
+  const updateURLWithResourceTypes = (
+      updatedResourceTypes: ResourceTypeEnum[]
+  ) => {
+    const params = new URLSearchParams(location.search);
+
+    if (updatedResourceTypes.length > 0) {
+      params.set(
+          "resourceTypes",
+          updatedResourceTypes.map((type) => type).join(",")
+      );
+    } else {
+      params.delete("resourceTypes");
+    }
+
+    navigate(
+        {pathname: location.pathname, search: params.toString()},
+        {replace: true}
+    );
+  };
+
+  const updateURLWithTags = (updatedTags: string[]) => {
+    const params = new URLSearchParams(location.search);
+
+    if (updatedTags.length > 0) {
+      params.set("tags", updatedTags.join(","));
+    } else {
+      params.delete("tags");
+    }
+
+    navigate(
+        {pathname: location.pathname, search: params.toString()},
+        {replace: true}
+    );
+  };
+
+  const updateURLWithSearchText = (searchText: string) => {
+    const params = new URLSearchParams(location.search);
+    if (searchText.length > 0) {
+      params.set("searchText", searchText);
+    } else {
+      params.delete("searchText");
+    }
+
+    navigate(
+        {pathname: location.pathname, search: params.toString()},
+        {replace: true}
+    );
+  }
+
+  useEffect(() => {
+    if (!hydrated) return;
+    setLoading(true);
+
+    const handler = setTimeout(() => {
+      updateURLWithSearchText(searchText);
+
+      async function fetchResources() {
+        try {
+          const resources = await getResources(resourceTypes, tags, 
searchText);
+          setResources(resources.content);
+        } catch {
+          toaster.create({
+            type: "error",
+            title: "Error fetching resources",
+            description: "An error occurred while fetching resources.",
+          });
+        } finally {
+          setLoading(false);
+        }
+      }
+
+      fetchResources();
+    }, 200);
+
+    return () => clearTimeout(handler);
+  }, [resourceTypes, tags, hydrated, searchText]);
+
+  useEffect(() => {
+    const params = new URLSearchParams(location.search);
+    const tagsParam = params.get("tags");
+    if (tagsParam) {
+      const initialTags = tagsParam.split(",");
+      setTags(initialTags);
+    } else {
+      setTags([]);
+    }
+
+    const resourceTypesParam = params.get("resourceTypes");
+    if (resourceTypesParam) {
+      const initialResourceTypes = resourceTypesParam.split(
+          ","
+      ) as ResourceTypeEnum[];
+      initialResourceTypes.forEach((type) => {
+        if (
+            !Object.values(ResourceTypeEnum).includes(type as ResourceTypeEnum)
+        ) {
+          toaster.create({
+            type: "error",
+            title: "Invalid resource type",
+            description: `Invalid resource type: ${type}. Valid types are: 
${Object.values(
+                ResourceTypeEnum
+            ).join(", ")}`,
+          });
+          return;
+        }
+      });
+
+      const searchTextParam = params.get("searchText");
+      if (searchTextParam) {
+        setSearchText(searchTextParam);
+      }
+
+      setResourceTypes(initialResourceTypes);
+    } else {
+      setResourceTypes([]);
+    }
+
+    setHydrated(true);
+  }, [location.search]);
+
+  useEffect(() => {
+    async function fetchTags() {
+      const tags: TagEntity[] = await getTags();
+      const suggestedTags = tags.map(tag => tag.value);
+
+      setSuggestions(suggestedTags);
+    }
+
+    fetchTags();
+  }, []);
+
+  return (
+      <>
+        <Box mt={4} maxWidth="1000px" mx="auto">
+          <VStack alignItems={'flex-start'}>
+            <Text fontSize="sm" color="gray.500" fontWeight="bold">
+              Search by resource title
+            </Text>
+            <Input
+                rounded={'lg'}
+                placeholder={'Search by resource title'}
+                value={searchText}
+                onChange={(e) => {
+                  setSearchText(e.target.value)
+                }}
+            />
+          </VStack>
+
+          <VStack mt={2} alignItems='flex-start'>
+            <Text fontSize="sm" color="gray.500" fontWeight="bold">
+              Tags Filter
+            </Text>
+
+            <HStack wrap={'wrap'}>
+              {
+                suggestions.map((tag) => {
+                  const isCurrentlyIncluded = tags.includes(tag);
+                  return (
+                      <Button
+                          key={tag}
+                          size={'xs'}
+                          bg={isCurrentlyIncluded ? 'blue.200' : "transparent"}
+                          color={"blue.600"}
+                          borderColor={'blue.400'}
+                          _hover={{
+                            bg: 'blue.400',
+                          }}
+                          rounded={'lg'}
+                          onClick={() => {
+                            setTags((prev) => {
+                              let newTags = [...prev, tag];
+                              if (isCurrentlyIncluded) {
+                                newTags = prev.filter((shouldKeepTag) => tag 
!= shouldKeepTag)
+                              }
+                              updateURLWithTags(newTags);
+                              return newTags;
+                            });
+                          }}
+                      >
+                        {tag}
+                      </Button>
+                  )
+                })
+              }
+            </HStack>
+
+          </VStack>
+
+
+          <VStack mt={2} alignItems='flex-start'>
+            <Text fontSize="sm" color="gray.500" fontWeight="bold">
+              Resource Filter
+            </Text>
+            <HStack wrap="wrap">
+              {labels.map((type) => {
+                const isSelected = resourceTypes.includes(type);
+                const color = resourceTypeToColor(type);
+                return (
+                    <Button
+                        key={type}
+                        variant="outline"
+                        color={color + ".600"}
+                        bg={isSelected ? color + ".100" : "white"}
+                        rounded={'lg'}
+                        _hover={{
+                          bg: isSelected ? color + ".200" : "gray.100",
+                          color: isSelected ? color + ".700" : "black",
+                        }}
+                        borderColor={color + ".200"}
+                        size="sm"
+                        onClick={() => {
+                          let newResourceTypes = [...resourceTypes, type];
+
+                          if (isSelected) {
+                            newResourceTypes = resourceTypes.filter(
+                                (t) => t !== type
+                            );
+                          }
+                          setResourceTypes(newResourceTypes);
+                          updateURLWithResourceTypes(newResourceTypes);
+                        }}
+                    >
+                      {type}
+                      {isSelected && <FaCheck color={color}/>}
+                    </Button>
+                );
+              })}
+            </HStack>
+          </VStack>
+        </Box>
+
+        {loading && (
+            <Box textAlign="center" mt={2}>
+              <Spinner size={'lg'}/>
+            </Box>
+        )}
+
+        {resources.length === 0 && (
+            <Box textAlign="center" color="gray.500">
+              <Text textAlign="center" mt={8} mb={4}>
+                No resources found with the following criteria:
+              </Text>
+              <Text>
+                Tags:{" "}
+                {tags.length > 0 ? (
+                    <>
+                      {tags.map((tag) => (
+                          <Code key={tag} colorScheme="blue" mr={1}>
+                            {tag}
+                          </Code>
+                      ))}
+                    </>
+                ) : (
+                    <Text as="span">None</Text>
+                )}
+              </Text>
+
+              <Text>
+                Resource Types:{" "}
+                {resourceTypes.length > 0 ? (
+                    <>
+                      {resourceTypes.map((type) => (
+                          <Code key={type} colorScheme="blue" mr={1}>
+                            {type}
+                          </Code>
+                      ))}
+                    </>
+                ) : (
+                    <Text as="span">None</Text>
+                )}
+              </Text>
+            </Box>
+        )}
+      </>
+  );
+};
\ No newline at end of file
diff --git a/airavata-research-portal/src/components/resources/index.tsx 
b/airavata-research-portal/src/components/resources/index.tsx
index 74f064dea..3b38e3229 100644
--- a/airavata-research-portal/src/components/resources/index.tsx
+++ b/airavata-research-portal/src/components/resources/index.tsx
@@ -1,201 +1,30 @@
-import {
-  Box,
-  Button,
-  Code,
-  Container,
-  Heading,
-  HStack,
-  Input,
-  SimpleGrid,
-  Spinner,
-  Text,
-  VStack,
-} from "@chakra-ui/react";
-import {useEffect, useState} from "react";
-import "./TagInput.css"; // 👈 custom styles
-import api from "@/lib/api";
-import {CONTROLLER} from "@/lib/controller";
-import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+import {Container, Heading, SimpleGrid, Text,} from "@chakra-ui/react";
+import {useState} from "react";
+import "./TagInput.css";
 import {Resource} from "@/interfaces/ResourceType";
 import {ResourceCard} from "../home/ResourceCard";
-import {FaCheck} from "react-icons/fa";
-import {Tag as TagEntity} from "@/interfaces/TagType";
-import {useLocation, useNavigate} from "react-router";
-import {toaster} from "../ui/toaster";
-import {resourceTypeToColor} from "@/lib/util";
-
-const getResources = async (
-    types: ResourceTypeEnum[],
-    stringTagsArr: string[],
-    searchText: string
-) => {
-  const response = await api.get(`${CONTROLLER.resources}/public`, {
-    params: {
-      type: types.join(","),
-      tag: stringTagsArr.join(","),
-      nameSearch: searchText,
-      pageNumber: 0,
-      pageSize: 100,
-    },
-  });
-  return response.data;
-};
-
-const getTags = async () => {
-  try {
-    const response = await api.get(`${CONTROLLER.resources}/public/tags/all`);
-    return response.data;
-  } catch (error) {
-    console.error("Error fetching:", error);
-  }
-};
+import {ResourceFilters} from "@/components/resources/ResourceFilters.tsx";
 
 export const Resources = () => {
-  const [tags, setTags] = useState<string[]>([]);
-  const [suggestions, setSuggestions] = useState<string[]>([]);
-  const [resourceTypes, setResourceTypes] = useState<ResourceTypeEnum[]>([]);
   const [resources, setResources] = useState<Resource[]>([]);
-  const [loading, setLoading] = useState(false);
-  const [hydrated, setHydrated] = useState(false);
-  const [searchText, setSearchText] = useState("");
-  const location = useLocation();
-  const navigate = useNavigate();
-
-  const updateURLWithTags = (updatedTags: string[]) => {
-    const params = new URLSearchParams(location.search);
-
-    if (updatedTags.length > 0) {
-      params.set("tags", updatedTags.join(","));
-    } else {
-      params.delete("tags");
-    }
-
-    navigate(
-        {pathname: location.pathname, search: params.toString()},
-        {replace: true}
-    );
-  };
-
-  const updateURLWithResourceTypes = (
-      updatedResourceTypes: ResourceTypeEnum[]
-  ) => {
-    const params = new URLSearchParams(location.search);
-
-    if (updatedResourceTypes.length > 0) {
-      params.set(
-          "resourceTypes",
-          updatedResourceTypes.map((type) => type).join(",")
-      );
-    } else {
-      params.delete("resourceTypes");
-    }
-
-    navigate(
-        {pathname: location.pathname, search: params.toString()},
-        {replace: true}
-    );
-  };
-
-  const updateURLWithSearchText = (searchText: string) => {
-    const params = new URLSearchParams(location.search);
-    if (searchText.length > 0) {
-      params.set("searchText", searchText);
-    } else {
-      params.delete("searchText");
-    }
-
-    navigate(
-        {pathname: location.pathname, search: params.toString()},
-        {replace: true}
-    );
-  }
-
-  useEffect(() => {
-    if (!hydrated) return;
-    setLoading(true);
-
-    const handler = setTimeout(() => {
-      async function fetchResources() {
-        try {
-          const resources = await getResources(resourceTypes, tags, 
searchText);
-          setResources(resources.content);
-        } catch {
-          toaster.create({
-            type: "error",
-            title: "Error fetching resources",
-            description: "An error occurred while fetching resources.",
-          });
-        } finally {
-          setLoading(false);
-        }
-      }
-
-      fetchResources();
-    }, 200);
-
-    return () => clearTimeout(handler);
-  }, [resourceTypes, tags, hydrated, searchText]);
-
-  useEffect(() => {
-    const params = new URLSearchParams(location.search);
-    const tagsParam = params.get("tags");
-    if (tagsParam) {
-      const initialTags = tagsParam.split(",");
-      setTags(initialTags);
-    } else {
-      setTags([]);
-    }
-
-    const resourceTypesParam = params.get("resourceTypes");
-    if (resourceTypesParam) {
-      const initialResourceTypes = resourceTypesParam.split(
-          ","
-      ) as ResourceTypeEnum[];
-      initialResourceTypes.forEach((type) => {
-        if (
-            !Object.values(ResourceTypeEnum).includes(type as ResourceTypeEnum)
-        ) {
-          toaster.create({
-            type: "error",
-            title: "Invalid resource type",
-            description: `Invalid resource type: ${type}. Valid types are: 
${Object.values(
-                ResourceTypeEnum
-            ).join(", ")}`,
-          });
-          return;
-        }
-      });
-
-      const searchTextParam = params.get("searchText");
-      if (searchTextParam) {
-        setSearchText(searchTextParam);
-      }
-
-      setResourceTypes(initialResourceTypes);
-    } else {
-      setResourceTypes([]);
-    }
-
-    setHydrated(true);
-  }, [location.search]);
-
-  useEffect(() => {
-    async function fetchTags() {
-      const tags: TagEntity[] = await getTags();
-      const suggestedTags = tags.map(tag => tag.value);
-
-      setSuggestions(suggestedTags);
-    }
-
-    fetchTags();
-  }, []);
-
-  const labels = [
-    ResourceTypeEnum.REPOSITORY,
-    ResourceTypeEnum.NOTEBOOK,
-    ResourceTypeEnum.DATASET,
-    ResourceTypeEnum.MODEL,
-  ];
 
   return (
       <>
@@ -218,110 +47,10 @@ export const Resources = () => {
             .
           </Text>
 
-          <Box mt={4} maxWidth="1000px" mx="auto">
-            <VStack alignItems={'flex-start'}>
-              <Text fontSize="sm" color="gray.500" fontWeight="bold">
-                Search by resource title
-              </Text>
-              <Input
-                  rounded={'lg'}
-                  placeholder={'Search by resource title'}
-                  value={searchText}
-                  onChange={(e) => {
-                    setSearchText(e.target.value)
-                    updateURLWithSearchText(e.target.value)
-                  }}
-              />
-            </VStack>
-
-            <VStack mt={2} alignItems='flex-start'>
-              <Text fontSize="sm" color="gray.500" fontWeight="bold">
-                Tags Filter
-              </Text>
-
-              <HStack wrap={'wrap'}>
-                {
-                  suggestions.map((tag) => {
-                    const isCurrentlyIncluded = tags.includes(tag);
-                    return (
-                        <Button
-                            key={tag}
-                            size={'xs'}
-                            bg={isCurrentlyIncluded ? 'blue.200' : 
"transparent"}
-                            color={"blue.600"}
-                            borderColor={'blue.400'}
-                            _hover={{
-                              bg: 'blue.400',
-                            }}
-                            rounded={'lg'}
-                            onClick={() => {
-                              setTags((prev) => {
-                                let newTags = [...prev, tag];
-                                if (isCurrentlyIncluded) {
-                                  newTags = prev.filter((shouldKeepTag) => tag 
!= shouldKeepTag)
-                                }
-                                updateURLWithTags(newTags);
-                                return newTags;
-                              });
-                            }}
-                        >
-                          {tag}
-                        </Button>
-                    )
-                  })
-                }
-              </HStack>
-
-            </VStack>
-
-
-            <VStack mt={2} alignItems='flex-start'>
-              <Text fontSize="sm" color="gray.500" fontWeight="bold">
-                Resource Filter
-              </Text>
-              <HStack wrap="wrap">
-                {labels.map((type) => {
-                  const isSelected = resourceTypes.includes(type);
-                  const color = resourceTypeToColor(type);
-                  return (
-                      <Button
-                          key={type}
-                          variant="outline"
-                          color={color + ".600"}
-                          bg={isSelected ? color + ".100" : "white"}
-                          rounded={'lg'}
-                          _hover={{
-                            bg: isSelected ? color + ".200" : "gray.100",
-                            color: isSelected ? color + ".700" : "black",
-                          }}
-                          borderColor={color + ".200"}
-                          size="sm"
-                          onClick={() => {
-                            let newResourceTypes = [...resourceTypes, type];
-
-                            if (isSelected) {
-                              newResourceTypes = resourceTypes.filter(
-                                  (t) => t !== type
-                              );
-                            }
-                            setResourceTypes(newResourceTypes);
-                            updateURLWithResourceTypes(newResourceTypes);
-                          }}
-                      >
-                        {type}
-                        {isSelected && <FaCheck color={color}/>}
-                      </Button>
-                  );
-                })}
-              </HStack>
-            </VStack>
-          </Box>
-
-          {loading && (
-              <Box textAlign="center" mt={2}>
-                <Spinner size={'lg'}/>
-              </Box>
-          )}
+          <ResourceFilters
+              resources={resources}
+              setResources={setResources}
+          />
 
           <SimpleGrid
               columns={{base: 1, md: 2, lg: 4}}
@@ -338,43 +67,6 @@ export const Resources = () => {
               );
             })}
           </SimpleGrid>
-
-          {resources.length === 0 && (
-              <Box textAlign="center" color="gray.500">
-                <Text textAlign="center" mt={8} mb={4}>
-                  No resources found with the following criteria:
-                </Text>
-                <Text>
-                  Tags:{" "}
-                  {tags.length > 0 ? (
-                      <>
-                        {tags.map((tag) => (
-                            <Code key={tag} colorScheme="blue" mr={1}>
-                              {tag}
-                            </Code>
-                        ))}
-                      </>
-                  ) : (
-                      <Text as="span">None</Text>
-                  )}
-                </Text>
-
-                <Text>
-                  Resource Types:{" "}
-                  {resourceTypes.length > 0 ? (
-                      <>
-                        {resourceTypes.map((type) => (
-                            <Code key={type} colorScheme="blue" mr={1}>
-                              {type}
-                            </Code>
-                        ))}
-                      </>
-                  ) : (
-                      <Text as="span">None</Text>
-                  )}
-                </Text>
-              </Box>
-          )}
         </Container>
       </>
   );

Reply via email to