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>
</>
);