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

dimuthuupe pushed a commit to branch cybershuttle-staging
in repository https://gitbox.apache.org/repos/asf/airavata.git

commit 8d7e4c439b051b8514202117299f884a9a5a613b
Author: ganning127 <[email protected]>
AuthorDate: Sun Apr 6 22:46:55 2025 -0400

    Condense to single resource page + allow unauthenticated routes
---
 .../research-framework/portal/package-lock.json    |  92 ++++++++-
 modules/research-framework/portal/package.json     |   4 +-
 modules/research-framework/portal/src/App.tsx      |  16 +-
 .../src/components/auth/ProtectedComponent.tsx     |  10 +-
 .../portal/src/components/auth/UserMenu.tsx        |   1 +
 .../portal/src/components/datasets/DatasetCard.tsx |  70 -------
 .../src/components/datasets/DatasetDetails.tsx     |  54 -----
 .../portal/src/components/home/ResourceCard.tsx    |  18 +-
 .../portal/src/components/notebooks/index.tsx      |  16 +-
 .../portal/src/components/resources/TagInput.css   | 103 ++++++++++
 .../portal/src/components/resources/index.tsx      | 226 +++++++++++++++++++++
 .../portal/src/layouts/NavBar.tsx                  |  38 ++--
 .../research-framework/research-service/pom.xml    |   4 +-
 .../research/service/config/AuthzTokenFilter.java  |   4 +-
 .../service/config/DevDataInitializer.java         |  29 ++-
 .../service/controller/ResourceController.java     |  14 +-
 .../research/service/handlers/ResourceHandler.java |  12 +-
 .../service/model/repo/ResourceRepository.java     |  16 ++
 18 files changed, 545 insertions(+), 182 deletions(-)

diff --git a/modules/research-framework/portal/package-lock.json 
b/modules/research-framework/portal/package-lock.json
index 1184b19f57..27f3a8f938 100644
--- a/modules/research-framework/portal/package-lock.json
+++ b/modules/research-framework/portal/package-lock.json
@@ -960,6 +960,21 @@
       "dev": true,
       "optional": true
     },
+    "@react-dnd/asap": {
+      "version": "5.0.2",
+      "resolved": 
"https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz";,
+      "integrity": 
"sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A=="
+    },
+    "@react-dnd/invariant": {
+      "version": "4.0.2",
+      "resolved": 
"https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz";,
+      "integrity": 
"sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw=="
+    },
+    "@react-dnd/shallowequal": {
+      "version": "4.0.2",
+      "resolved": 
"https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz";,
+      "integrity": 
"sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA=="
+    },
     "@rollup/rollup-android-arm-eabi": {
       "version": "4.35.0",
       "resolved": 
"https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz";,
@@ -1180,6 +1195,19 @@
       "integrity": 
"sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
       "dev": true
     },
+    "@types/lodash": {
+      "version": "4.17.16",
+      "resolved": 
"https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz";,
+      "integrity": 
"sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="
+    },
+    "@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": 
"https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz";,
+      "integrity": 
"sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "requires": {
+        "@types/lodash": "*"
+      }
+    },
     "@types/ms": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz";,
@@ -2450,6 +2478,11 @@
         }
       }
     },
+    "classnames": {
+      "version": "2.3.3",
+      "resolved": 
"https://registry.npmjs.org/classnames/-/classnames-2.3.3.tgz";,
+      "integrity": 
"sha512-1inzZmicIFcmUya7PGtUQeXtcF7zZpPnxtQoYOrz0uiOBGlLFa4ik4361seYL2JCcRDIyfdFHiwQolESFlw+Og=="
+    },
     "cli-table": {
       "version": "0.3.11",
       "resolved": 
"https://registry.npmjs.org/cli-table/-/cli-table-0.3.11.tgz";,
@@ -2564,6 +2597,16 @@
       "resolved": 
"https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz";,
       "integrity": 
"sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
     },
+    "dnd-core": {
+      "version": "16.0.1",
+      "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz";,
+      "integrity": 
"sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
+      "requires": {
+        "@react-dnd/asap": "^5.0.1",
+        "@react-dnd/invariant": "^4.0.1",
+        "redux": "^4.2.0"
+      }
+    },
     "dunder-proto": {
       "version": "1.0.1",
       "resolved": 
"https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz";,
@@ -2788,8 +2831,7 @@
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": 
"https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz";,
-      "integrity": 
"sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true
+      "integrity": 
"sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
     },
     "fast-glob": {
       "version": "3.3.3",
@@ -3294,6 +3336,11 @@
         "p-locate": "^5.0.0"
       }
     },
+    "lodash-es": {
+      "version": "4.17.21",
+      "resolved": 
"https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz";,
+      "integrity": 
"sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+    },
     "lodash.merge": {
       "version": "4.6.2",
       "resolved": 
"https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz";,
@@ -3651,6 +3698,26 @@
       "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz";,
       "integrity": 
"sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="
     },
+    "react-dnd": {
+      "version": "16.0.1",
+      "resolved": 
"https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz";,
+      "integrity": 
"sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
+      "requires": {
+        "@react-dnd/invariant": "^4.0.1",
+        "@react-dnd/shallowequal": "^4.0.1",
+        "dnd-core": "^16.0.1",
+        "fast-deep-equal": "^3.1.3",
+        "hoist-non-react-statics": "^3.3.2"
+      }
+    },
+    "react-dnd-html5-backend": {
+      "version": "16.0.1",
+      "resolved": 
"https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz";,
+      "integrity": 
"sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
+      "requires": {
+        "dnd-core": "^16.0.1"
+      }
+    },
     "react-dom": {
       "version": "19.0.0",
       "resolved": 
"https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz";,
@@ -3691,10 +3758,15 @@
         "turbo-stream": "2.4.0"
       }
     },
-    "react-tag-input-component": {
-      "version": "2.0.2",
-      "resolved": 
"https://registry.npmjs.org/react-tag-input-component/-/react-tag-input-component-2.0.2.tgz";,
-      "integrity": 
"sha512-dydI9luVwwv9vrjE5u1TTnkcOVkOVL6mhFti8r6hLi78V2F2EKWQOLptURz79UYbDHLSk6tnbvGl8FE+sMpADg=="
+    "react-tag-input": {
+      "version": "6.10.6",
+      "resolved": 
"https://registry.npmjs.org/react-tag-input/-/react-tag-input-6.10.6.tgz";,
+      "integrity": 
"sha512-oXopRSs2ZSuMqsygcZhjal869MZpAUX0YVNQ/WGaYXPJmbuZIviqeTocs3g/BHs9ReIG9vu563JOZsHGikiMgw==",
+      "requires": {
+        "@types/lodash-es": "^4.17.12",
+        "classnames": "~2.3.1",
+        "lodash-es": "^4.17.21"
+      }
     },
     "readdirp": {
       "version": "3.6.0",
@@ -3705,6 +3777,14 @@
         "picomatch": "^2.2.1"
       }
     },
+    "redux": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz";,
+      "integrity": 
"sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+      "requires": {
+        "@babel/runtime": "^7.9.2"
+      }
+    },
     "regenerator-runtime": {
       "version": "0.14.1",
       "resolved": 
"https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz";,
diff --git a/modules/research-framework/portal/package.json 
b/modules/research-framework/portal/package.json
index da96f656f9..65121a7cf7 100644
--- a/modules/research-framework/portal/package.json
+++ b/modules/research-framework/portal/package.json
@@ -19,11 +19,13 @@
     "next-themes": "^0.4.6",
     "oidc-client-ts": "^3.2.0",
     "react": "^19.0.0",
+    "react-dnd": "^16.0.1",
+    "react-dnd-html5-backend": "^16.0.1",
     "react-dom": "^19.0.0",
     "react-icons": "^5.5.0",
     "react-oidc-context": "^3.3.0",
     "react-router": "^7.3.0",
-    "react-tag-input-component": "^2.0.2"
+    "react-tag-input": "^6.10.6"
   },
   "devDependencies": {
     "@chakra-ui/cli": "^3.12.0",
diff --git a/modules/research-framework/portal/src/App.tsx 
b/modules/research-framework/portal/src/App.tsx
index 32b403aacc..c59ac8ad91 100644
--- a/modules/research-framework/portal/src/App.tsx
+++ b/modules/research-framework/portal/src/App.tsx
@@ -19,6 +19,7 @@ import {
   OPENID_CONFIG_URL,
 } from "./lib/constants";
 import { WebStorageStateStore } from "oidc-client-ts";
+import { Resources } from "./components/resources";
 function App() {
   const colorMode = useColorMode();
   const navigate = useNavigate();
@@ -73,10 +74,6 @@ function App() {
       <AuthProvider
         {...oidcConfig}
         onSigninCallback={() => {
-          // const from = location.state?.from || "/"; // fallback to homepage
-          // console.log("Redirecting to:", from);
-          // navigate(from, { replace: true });
-          // clear state from url
           navigate(location.pathname, { replace: true });
         }}
       >
@@ -85,6 +82,12 @@ function App() {
           <Route element={<NavBarFooterLayout />}>
             <Route path="/" element={<CybershuttleLanding />} />
             <Route path="/login" element={<Login />} />
+            <Route path="/resources" element={<Resources />} />
+            <Route path="/resources/datasets" element={<Datasets />} />
+            <Route path="/resources/notebooks" element={<Notebooks />} />
+            <Route path="/resources/repositories" element={<Repositories />} />
+            <Route path="/resources/models" element={<Models />} />
+            <Route path="/resources/:type/:id" element={<ResourceDetails />} />
           </Route>
 
           {/* Protected Routes with Layout */}
@@ -92,11 +95,6 @@ function App() {
             element={<ProtectedComponent Component={NavBarFooterLayout} />}
           >
             <Route path="/projects" element={<Home />} />
-            <Route path="/resources/notebooks" element={<Notebooks />} />
-            <Route path="/resources/datasets" element={<Datasets />} />
-            <Route path="/resources/repositories" element={<Repositories />} />
-            <Route path="/resources/models" element={<Models />} />
-            <Route path="/resources/:type/:id" element={<ResourceDetails />} />
           </Route>
         </Routes>
       </AuthProvider>
diff --git 
a/modules/research-framework/portal/src/components/auth/ProtectedComponent.tsx 
b/modules/research-framework/portal/src/components/auth/ProtectedComponent.tsx
index d9a27a7c44..ed2685ab9f 100644
--- 
a/modules/research-framework/portal/src/components/auth/ProtectedComponent.tsx
+++ 
b/modules/research-framework/portal/src/components/auth/ProtectedComponent.tsx
@@ -1,25 +1,25 @@
 import { setUserProvider } from "@/lib/api";
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
 import { useAuth } from "react-oidc-context";
 import { useNavigate } from "react-router";
 
 function ProtectedComponent({ Component }: { Component: React.FC }) {
   const auth = useAuth();
   const navigate = useNavigate();
-  const path = window.location.pathname;
+  const initialPathRef = useRef(window.location.pathname); // store once
 
   useEffect(() => {
     if (!auth.isLoading && !auth.isAuthenticated) {
-      navigate(`/login?redirect=${path}`, { replace: true });
+      navigate(`/login?redirect=${initialPathRef.current}`, { replace: true });
     }
 
     if (auth.isAuthenticated) {
       setUserProvider(() => Promise.resolve(auth.user ?? null));
     }
-  }, [auth]);
+  }, [auth, navigate]);
 
   if (auth.isLoading || !auth.isAuthenticated) {
-    return;
+    return null;
   }
 
   return <Component />;
diff --git a/modules/research-framework/portal/src/components/auth/UserMenu.tsx 
b/modules/research-framework/portal/src/components/auth/UserMenu.tsx
index cba709720f..a92ff98eac 100644
--- a/modules/research-framework/portal/src/components/auth/UserMenu.tsx
+++ b/modules/research-framework/portal/src/components/auth/UserMenu.tsx
@@ -24,6 +24,7 @@ export const UserMenu = () => {
       </Link>
     );
   const handleLogout = async () => {
+    // Clear the user provider
     await auth.signoutRedirect();
   };
 
diff --git 
a/modules/research-framework/portal/src/components/datasets/DatasetCard.tsx 
b/modules/research-framework/portal/src/components/datasets/DatasetCard.tsx
deleted file mode 100644
index 95d3a84ddf..0000000000
--- a/modules/research-framework/portal/src/components/datasets/DatasetCard.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { DatasetType } from "@/interfaces/DatasetType";
-import {
-  Text,
-  Box,
-  Card,
-  HStack,
-  Avatar,
-  Image,
-  Badge,
-} from "@chakra-ui/react";
-import { Link } from "react-router";
-
-export const DatasetCard = ({ dataset }: { dataset: DatasetType }) => {
-  return (
-    <Box>
-      <Card.Root overflow="hidden">
-        <Image
-          src={dataset.metadata.images.headerImage}
-          maxH={100}
-          alt="Green double couch with wooden legs"
-        />
-        <Link to={`/datasets/${dataset.metadata.slug}`}>
-          <Card.Body
-            gap="2"
-            _hover={{
-              bg: "gray.100",
-            }}
-          >
-            <HStack justifyContent="space-between" mb={1}>
-              <Card.Title>{dataset.metadata.title}</Card.Title>
-              <Badge
-                colorPalette={dataset.private ? "red" : "green"}
-                rounded="md"
-              >
-                {dataset.private ? "Private" : "Public"}
-              </Badge>
-            </HStack>
-            <HStack flexWrap={"wrap"}>
-              <Badge colorPalette={"purple"} fontWeight="bold" size="md">
-                {dataset.metadata.type}
-              </Badge>
-              {dataset.metadata.tags.map((tag: string) => (
-                <Badge size="md" key={tag}>
-                  {tag}
-                </Badge>
-              ))}
-            </HStack>
-            <Text color="fg.muted" lineClamp={2}>
-              {dataset.metadata.description}.
-            </Text>
-          </Card.Body>
-        </Link>
-
-        <Card.Footer justifyContent="space-between">
-          <HStack mt={4}>
-            <Avatar.Root shape="full" size="xl">
-              <Avatar.Fallback name={dataset.metadata.author.name} />
-              <Avatar.Image src={dataset.metadata.author.avatar} />
-            </Avatar.Root>
-
-            <Box>
-              <Text fontWeight="bold">{dataset.metadata.author.name}</Text>
-              <Text color="gray.500">{dataset.metadata.author.role}</Text>
-            </Box>
-          </HStack>
-        </Card.Footer>
-      </Card.Root>
-    </Box>
-  );
-};
diff --git 
a/modules/research-framework/portal/src/components/datasets/DatasetDetails.tsx 
b/modules/research-framework/portal/src/components/datasets/DatasetDetails.tsx
deleted file mode 100644
index 975639fcd6..0000000000
--- 
a/modules/research-framework/portal/src/components/datasets/DatasetDetails.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Metadata } from "../Metadata";
-import NavBar from "../../layouts/NavBar";
-// @ts-expect-error This is fine
-import { MOCK_DATASETS } from "../../data/MOCK_DATA";
-import { DatasetType } from "@/interfaces/DatasetType";
-import { useEffect, useState } from "react";
-import { Badge, Box, Container, HStack, Icon, Spinner } from 
"@chakra-ui/react";
-import { Link, useParams } from "react-router";
-import { BiArrowBack } from "react-icons/bi";
-
-async function getDataset(slug: string | undefined) {
-  return MOCK_DATASETS.find(
-    (dataset: DatasetType) => dataset.metadata.slug === slug
-  );
-}
-
-export const DatasetDetails = () => {
-  const [dataset, setDataset] = useState<DatasetType | null>(null);
-  const { slug } = useParams();
-
-  useEffect(() => {
-    async function getData() {
-      const n = await getDataset(slug);
-      setDataset(n);
-    }
-    getData();
-  }, []);
-
-  if (!dataset) return <Spinner />;
-  return (
-    <>
-      <NavBar />
-
-      <Container maxW="breakpoint-lg" mx="auto" p={4} mt={16}>
-        <Box>
-          <HStack alignItems="center" mb={4}>
-            <Icon>
-              <BiArrowBack />
-            </Icon>
-            <Link to="/datasets">Back to Datasets</Link>
-          </HStack>
-        </Box>
-        <Badge
-          colorPalette={dataset.private ? "red" : "green"}
-          rounded="md"
-          mb={1}
-        >
-          {dataset.private ? "Private" : "Public"}
-        </Badge>
-        <Metadata metadata={dataset.metadata} />
-      </Container>
-    </>
-  );
-};
diff --git 
a/modules/research-framework/portal/src/components/home/ResourceCard.tsx 
b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
index a5da829aac..71f937cfdd 100644
--- a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
+++ b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
@@ -15,15 +15,29 @@ import { ResourceTypeBadge } from 
"../resources/ResourceTypeBadge";
 import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
 import { ModelCardButton } from "../models/ModelCardButton";
 
-export const ResourceCard = ({ resource }: { resource: Resource }) => {
+export const ResourceCard = ({
+  resource,
+  appendTypeToUrl = false,
+}: {
+  resource: Resource;
+  appendTypeToUrl?: boolean;
+}) => {
   const author = resource.authors[0];
 
   const isValidImage = isValidImaage(resource.headerImage);
 
+  resource.tags.sort((a, b) => a.value.localeCompare(b.value));
+
+  const linkTo = resource.id;
+  const linkToWithType = `${resource.type}/${resource.id}`;
+
+  // Determine the link based on appendTypeToUrl
+  const link = appendTypeToUrl ? linkToWithType : linkTo;
+
   return (
     <Box>
       <Card.Root overflow="hidden" size="md">
-        <Link to={`${resource.id}`}>
+        <Link to={link}>
           {/* Image Container with Badge */}
           {isValidImage && (
             <Box position="relative" width="full">
diff --git 
a/modules/research-framework/portal/src/components/notebooks/index.tsx 
b/modules/research-framework/portal/src/components/notebooks/index.tsx
index ac0dd44507..cb241577ea 100644
--- a/modules/research-framework/portal/src/components/notebooks/index.tsx
+++ b/modules/research-framework/portal/src/components/notebooks/index.tsx
@@ -47,13 +47,7 @@ const Notebooks = () => {
         <InputGroup endElement={<LuSearch />} w="100%" mt={4}>
           <Input placeholder="Search" rounded="md" />
         </InputGroup>
-        {/* <Box mt={4}>
-          <TagsInput
-            value={tags}
-            onChange={setTags}
-            placeHolder="Filter by tags"
-          />
-        </Box> */}
+
         <SimpleGrid
           columns={{ base: 1, md: 2, lg: 3 }}
           mt={4}
@@ -61,7 +55,13 @@ const Notebooks = () => {
           justifyContent="space-around"
         >
           {notebooks.map((notebook: NotebookResource) => {
-            return <ResourceCard resource={notebook} key={notebook.id} />;
+            return (
+              <ResourceCard
+                resource={notebook}
+                key={notebook.id}
+                appendTypeToUrl={false}
+              />
+            );
           })}
         </SimpleGrid>
       </Container>
diff --git 
a/modules/research-framework/portal/src/components/resources/TagInput.css 
b/modules/research-framework/portal/src/components/resources/TagInput.css
new file mode 100644
index 0000000000..9caa43e09c
--- /dev/null
+++ b/modules/research-framework/portal/src/components/resources/TagInput.css
@@ -0,0 +1,103 @@
+/* TagInput.css */
+
+.tag-input-wrapper {
+  padding: 1rem;
+  border-radius: 12px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
+}
+
+/* container holding all tags + input */
+.ReactTags__tags {
+  position: relative;
+  /* Add this line */
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  border: 2px solid #e0e0e0;
+  border-radius: 10px;
+  padding: 10px;
+  background-color: #fafafa;
+  transition: border-color 0.2s ease-in-out;
+}
+
+/* tag pill style */
+.ReactTags__tag {
+  background-color: #007bff;
+  color: #fff;
+  padding: 6px 12px;
+  border-radius: 20px;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  white-space: nowrap;
+}
+
+/* remove (x) icon */
+.ReactTags__remove {
+  margin-left: 4px;
+  cursor: pointer;
+  font-weight: bold;
+  color: #fff;
+}
+
+/* input field container (flexed) */
+.ReactTags__tagInput {
+  flex: 1 1 auto;
+  /* allow it to grow and wrap */
+  min-width: 120px;
+}
+
+.ReactTags__selected {
+  width: 100%;
+  display: flex;
+  gap: 0.5rem;
+}
+
+/* actual input element */
+.ReactTags__tagInputField {
+  width: 100%;
+  border: none;
+  outline: none;
+  font-size: 14px;
+  background: transparent;
+  padding: 6px;
+  box-sizing: border-box;
+}
+
+.ReactTags__tagInputField::placeholder {
+  color: #888;
+}
+
+/* Make suggestions dropdown absolutely positioned */
+.ReactTags__suggestions {
+  position: absolute;
+  top: 100%;
+  /* Push below the input */
+  left: 0;
+  width: 100%;
+  /* Match parent width */
+  z-index: 10;
+  background-color: #fff;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  max-height: 200px;
+  overflow-y: auto;
+  list-style: none;
+  margin-top: 4px;
+  padding: 0;
+}
+
+/* Each individual suggestion */
+.ReactTags__suggestions li {
+  padding: 8px 12px;
+  cursor: pointer;
+  transition: background-color 0.2s ease;
+}
+
+/* Active (highlighted) suggestion */
+.ReactTags__activeSuggestion {
+  background-color: #007bff;
+  color: white;
+}
\ No newline at end of file
diff --git 
a/modules/research-framework/portal/src/components/resources/index.tsx 
b/modules/research-framework/portal/src/components/resources/index.tsx
new file mode 100644
index 0000000000..504ed2899e
--- /dev/null
+++ b/modules/research-framework/portal/src/components/resources/index.tsx
@@ -0,0 +1,226 @@
+import {
+  Text,
+  Container,
+  Heading,
+  Box,
+  SimpleGrid,
+  Button,
+  HStack,
+  Code,
+} from "@chakra-ui/react";
+import { useEffect, useState } from "react";
+import { SEPARATORS, WithContext as ReactTags, Tag } from "react-tag-input";
+import "./TagInput.css"; // 👈 custom styles
+import api from "@/lib/api";
+import { CONTROLLER } from "@/lib/controller";
+import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
+import { Resource } from "@/interfaces/ResourceType";
+import { ResourceCard } from "../home/ResourceCard";
+import { FaCheck } from "react-icons/fa";
+import { Tag as TagEntity } from "@/interfaces/TagType";
+
+const getResources = async (
+  types: ResourceTypeEnum[],
+  stringTagsArr: string[]
+) => {
+  try {
+    const response = await api.get(`${CONTROLLER.resources}/`, {
+      params: {
+        type: types.join(","),
+        tag: stringTagsArr.join(","),
+        pageNumber: 0,
+        pageSize: 100,
+      },
+    });
+    const data = response.data;
+    return data;
+  } catch (error) {
+    console.error("Error fetching:", error);
+  }
+};
+
+const getTags = async () => {
+  try {
+    const response = await api.get(`${CONTROLLER.resources}/tags/all`);
+    const data = response.data;
+    return data;
+  } catch (error) {
+    console.error("Error fetching:", error);
+  }
+};
+
+export const Resources = () => {
+  const [tags, setTags] = useState<Tag[]>([]);
+  const [suggestions, setSuggestions] = useState<Tag[]>([]);
+  const [resourceTypes, setResourceTypes] = useState<ResourceTypeEnum[]>([
+    ResourceTypeEnum.DATASET,
+    ResourceTypeEnum.REPOSITORY,
+    ResourceTypeEnum.NOTEBOOK,
+    ResourceTypeEnum.MODEL,
+  ]);
+  const [resources, setResources] = useState<Resource[]>([]);
+
+  const handleDelete = (i: number) => {
+    setTags(tags.filter((tag, index) => index !== i));
+  };
+
+  const handleAddition = (tag: Tag) => {
+    console.log(tag);
+    setTags([...tags, tag]);
+  };
+
+  useEffect(() => {
+    async function fetchResources() {
+      const stringTagsArr = tags.map((tag) => tag.text);
+      const resources = await getResources(resourceTypes, stringTagsArr);
+      setResources(resources.content);
+    }
+
+    fetchResources();
+  }, [resourceTypes, tags]);
+
+  useEffect(() => {
+    async function fetchTags() {
+      const tags: TagEntity[] = await getTags();
+      const suggestedTags = tags.map((tag: TagEntity) => ({
+        id: tag.value,
+        text: tag.value,
+        className: "",
+      }));
+
+      setSuggestions(suggestedTags);
+    }
+
+    fetchTags();
+  }, []);
+
+  return (
+    <>
+      <Container maxW="container.lg" mt={8}>
+        <Heading
+          textAlign="center"
+          fontSize={{ base: "4xl", md: "5xl" }}
+          fontWeight="black"
+          lineHeight={1.2}
+        >
+          Browse datasets, repositories, notebooks, and models,
+          <Text as="span" color="blue.600">
+            {" "}
+            made by scientists, for scientists
+          </Text>
+          .
+        </Heading>
+
+        <Box mt={4} maxWidth="1000px" mx="auto">
+          <ReactTags
+            tags={tags}
+            handleDelete={handleDelete}
+            handleAddition={handleAddition}
+            suggestions={suggestions}
+            separators={[
+              SEPARATORS.TAB,
+              SEPARATORS.COMMA,
+              SEPARATORS.ENTER,
+              SEPARATORS.SEMICOLON,
+            ]}
+            allowDragDrop={true}
+            placeholder="Filter resources by tags"
+            renderSuggestion={(item) => {
+              return <span>{item.text}</span>;
+            }}
+          />
+
+          <HStack alignItems="center" mt={2}>
+            <Text fontSize="sm" color="gray.500" fontWeight="bold">
+              Showing
+            </Text>
+            <HStack wrap="wrap">
+              {Object.values(ResourceTypeEnum).map((type) => {
+                const isSelected = resourceTypes.includes(type);
+                return (
+                  <Button
+                    key={type}
+                    variant="outline"
+                    color={isSelected ? "blue.600" : "black"}
+                    bg={isSelected ? "blue.100" : "white"}
+                    _hover={{
+                      bg: isSelected ? "blue.200" : "gray.100",
+                      color: isSelected ? "blue.700" : "black",
+                    }}
+                    size="sm"
+                    onClick={() => {
+                      if (isSelected) {
+                        setResourceTypes(
+                          resourceTypes.filter((t) => t !== type)
+                        );
+                      } else {
+                        setResourceTypes([...resourceTypes, type]);
+                      }
+                    }}
+                  >
+                    {type}
+                    {isSelected && <FaCheck color="blue" />}
+                  </Button>
+                );
+              })}
+            </HStack>
+          </HStack>
+        </Box>
+
+        <SimpleGrid
+          columns={{ base: 1, md: 2, lg: 3 }}
+          mt={4}
+          gap={4}
+          justifyContent="space-around"
+        >
+          {resources.map((resource: Resource) => {
+            return (
+              <ResourceCard
+                resource={resource}
+                key={resource.id}
+                appendTypeToUrl={true}
+              />
+            );
+          })}
+        </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.id} colorScheme="blue" mr={1}>
+                      {tag.text}
+                    </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>
+    </>
+  );
+};
diff --git a/modules/research-framework/portal/src/layouts/NavBar.tsx 
b/modules/research-framework/portal/src/layouts/NavBar.tsx
index 10b5537f49..016adb640c 100644
--- a/modules/research-framework/portal/src/layouts/NavBar.tsx
+++ b/modules/research-framework/portal/src/layouts/NavBar.tsx
@@ -20,24 +20,28 @@ import { UserMenu } from "@/components/auth/UserMenu";
 
 const NAV_CONTENT = [
   {
-    title: "Projects",
-    url: "/projects",
-  },
-  {
-    title: "Datasets",
-    url: "/resources/datasets",
-  },
-  {
-    title: "Repositories",
-    url: "/resources/repositories",
-  },
-  {
-    title: "Notebooks",
-    url: "/resources/notebooks",
+    title: "Resources",
+    url: "/resources",
   },
+  // {
+  //   title: "Datasets",
+  //   url: "/resources/datasets",
+  // },
+  // {
+  //   title: "Repositories",
+  //   url: "/resources/repositories",
+  // },
+  // {
+  //   title: "Notebooks",
+  //   url: "/resources/notebooks",
+  // },
+  // {
+  //   title: "Models",
+  //   url: "/resources/models",
+  // },
   {
-    title: "Models",
-    url: "/resources/models",
+    title: "Projects",
+    url: "/projects",
   },
 ];
 
@@ -82,7 +86,7 @@ const NavBar = () => {
         </IconButton>
 
         {/* Logo */}
-        <Link to="/projects">
+        <Link to="/">
           <Image src={ApacheAiravataLogo} alt="Logo" boxSize="30px" />
         </Link>
 
diff --git a/modules/research-framework/research-service/pom.xml 
b/modules/research-framework/research-service/pom.xml
index d79af13145..0cea8263b8 100644
--- a/modules/research-framework/research-service/pom.xml
+++ b/modules/research-framework/research-service/pom.xml
@@ -178,8 +178,8 @@
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
-                    <source>14</source>
-                    <target>14</target>
+                    <source>15</source>
+                    <target>15</target>
                 </configuration>
             </plugin>
             <plugin>
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
index d31cc28c37..f2c15f0ec5 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
@@ -56,12 +56,14 @@ public class AuthzTokenFilter extends OncePerRequestFilter {
     @Override
     protected boolean shouldNotFilter(HttpServletRequest request) throws 
ServletException {
         String path = request.getRequestURI();
+        // TODO: ensure that only GET requests do not need auth
         return path.startsWith("/swagger") ||
                 path.startsWith("/v2/api-docs") ||
                 path.startsWith("/v3/api-docs") ||
                 path.startsWith("/swagger-ui") ||
                 path.startsWith("/swagger-resources") ||
-                path.startsWith("/webjars/");
+                path.startsWith("/webjars/") ||
+                path.startsWith("/api/v1/rf/resources");
     }
 
     @Override
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
index 35614509c7..816fa83e72 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
@@ -18,14 +18,18 @@
 package org.apache.airavata.research.service.config;
 
 import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
 
 import org.apache.airavata.research.service.enums.PrivacyEnum;
 import org.apache.airavata.research.service.enums.StatusEnum;
 import org.apache.airavata.research.service.model.entity.DatasetResource;
 import org.apache.airavata.research.service.model.entity.Project;
 import org.apache.airavata.research.service.model.entity.RepositoryResource;
+import org.apache.airavata.research.service.model.entity.Tag;
 import org.apache.airavata.research.service.model.repo.ProjectRepository;
 import org.apache.airavata.research.service.model.repo.ResourceRepository;
+import org.apache.airavata.research.service.model.repo.TagRepository;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.context.annotation.Profile;
@@ -37,16 +41,31 @@ public class DevDataInitializer implements 
CommandLineRunner {
 
     private final ProjectRepository projectRepository;
     private final ResourceRepository resourceRepository;
+    private final TagRepository tagRepository;
 
     @Value("${airavata.research-hub.dev-user}")
     private String devUserEmail;
 
-    public DevDataInitializer(ProjectRepository projectRepository, 
ResourceRepository resourceRepository) {
+    public DevDataInitializer(ProjectRepository projectRepository, 
ResourceRepository resourceRepository, TagRepository tagRepository) {
         this.projectRepository = projectRepository;
         this.resourceRepository = resourceRepository;
+        this.tagRepository = tagRepository;
     }
 
-    private void createProject(String name, String repoUrl, String 
datasetName, String datasetUrl, String user) {
+    private void createProject(String name, String repoUrl, String 
datasetName, String datasetUrl,  String[] tags, String user) {
+        Set<Tag> tagSet = new HashSet<>();
+        for (String tag : tags) {
+            Tag t = tagRepository.findByValue(tag);
+            if (t != null) {
+                tagSet.add(t);
+            } else {
+                Tag newTag = new Tag();
+                newTag.setValue(tag);
+                tagSet.add(newTag);
+                tagRepository.save(newTag);
+            }
+        }
+
         RepositoryResource repo = new RepositoryResource();
         repo.setName(name);
         repo.setDescription("Repository for " + name);
@@ -54,6 +73,7 @@ public class DevDataInitializer implements CommandLineRunner {
         repo.setRepositoryUrl(repoUrl);
         repo.setStatus(StatusEnum.VERIFIED);
         repo.setPrivacy(PrivacyEnum.PUBLIC);
+        repo.setTags(tagSet);
         repo = resourceRepository.save(repo);
 
         DatasetResource dataset = new DatasetResource();
@@ -63,6 +83,7 @@ public class DevDataInitializer implements CommandLineRunner {
         dataset.setDatasetUrl(datasetUrl);
         dataset.setStatus(StatusEnum.VERIFIED);
         dataset.setPrivacy(PrivacyEnum.PUBLIC);
+        dataset.setTags(tagSet);
         dataset.setAuthors(new HashSet<>() {
             {
                 add(user);
@@ -92,6 +113,7 @@ public class DevDataInitializer implements CommandLineRunner 
{
                 "https://github.com/yasithdev/bmtk-workshop.git";,
                 "Allen / BMTK Workshop Data",
                 "allen-bmtk-workshop",
+                new String[]{"allen", "bmtk", "workshop"},
                 devUserEmail
         );
 
@@ -100,6 +122,7 @@ public class DevDataInitializer implements 
CommandLineRunner {
                 "https://github.com/yasithdev/allen-v1.git";,
                 "Allen / V1 Data",
                 "allen-v1",
+                new String[]{"allen", "v1", "workshop"},
                 devUserEmail
         );
 
@@ -108,6 +131,7 @@ public class DevDataInitializer implements 
CommandLineRunner {
                 "https://github.com/yasithdev/onehot-hmmglm.git";,
                 "BRAINML / OneHot HMMGLM Data",
                 "brainml-onehot-hmmglm",
+                new String[]{"brainml", "onehot", "workshop"},
                 devUserEmail
         );
 
@@ -116,6 +140,7 @@ public class DevDataInitializer implements 
CommandLineRunner {
                 "https://github.com/yasithdev/functional-network.git";,
                 "HChoiLab / Functional Network Data",
                 "hchoilab-functional-network",
+                new String[]{"hchoilab", "functional network", "workshop"},
                 devUserEmail
         );
     }
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
index 9f4228b554..d7116476d2 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResourceController.java
@@ -73,6 +73,14 @@ public class ResourceController {
         return ResponseEntity.ok(response);
     }
 
+    @Operation(
+            summary = "Get all tags"
+    )
+    @GetMapping(value = "/tags/all")
+    public 
ResponseEntity<List<org.apache.airavata.research.service.model.entity.Tag>> 
getTags() {
+        return ResponseEntity.ok(resourceHandler.getAllTags());
+    }
+
     @Operation(
             summary = "Get dataset, notebook, or repository"
     )
@@ -86,10 +94,10 @@ public class ResourceController {
     )
     @GetMapping("/")
     public ResponseEntity<Page<Resource>> getAllResources(
-            @RequestHeader(name="X-Claims", required=true) String claims,
             @RequestParam(value="pageNumber", defaultValue = "0") int 
pageNumber,
             @RequestParam(value="pageSize", defaultValue = "10") int pageSize,
-            @RequestParam(value="type") ResourceTypeEnum[] types
+            @RequestParam(value="type") ResourceTypeEnum[] types,
+            @RequestParam(value="tag", required = false) String[] tags
     ) {
         List<Class<? extends Resource>> typeList = new ArrayList<>();
         for (ResourceTypeEnum resourceType : types) {
@@ -103,7 +111,7 @@ public class ResourceController {
                 typeList.add(DatasetResource.class);
             }
         }
-        Page<Resource> response = resourceHandler.getAllResources(pageNumber, 
pageSize, typeList);
+        Page<Resource> response = resourceHandler.getAllResources(pageNumber, 
pageSize, typeList, tags);
 
         return ResponseEntity.ok(response);
     }
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
index 85587ea507..72c36bb19c 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
@@ -100,9 +100,17 @@ public class ResourceHandler {
         return opResource.get();
     }
 
-    public Page<Resource> getAllResources(int pageNumber, int pageSize, 
List<Class<? extends Resource>> typeList) {
+    public Page<Resource> getAllResources(int pageNumber, int pageSize, 
List<Class<? extends Resource>> typeList, String[] tag) {
         Pageable pageable = PageRequest.of(pageNumber, pageSize);
-        return resourceRepository.findAllByTypes(typeList, pageable);
+        if (tag == null || tag.length == 0) {
+            return resourceRepository.findAllByTypes(typeList, pageable);
+        }
+
+        return resourceRepository.findAllByTypesAndAllTags(typeList, tag, 
tag.length, pageable);
+    }
+
+    public List<Tag> getAllTags() {
+        return tagRepository.findAll();
     }
 
     // Helper methods
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
index 72daa3c787..95c8205ce2 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
@@ -33,4 +33,20 @@ public interface ResourceRepository extends 
JpaRepository<Resource, String> {
 
     @Query("SELECT r FROM #{#entityName} r WHERE TYPE(r) IN :types")
     Page<Resource> findAllByTypes(@Param("types") List<Class<? extends 
Resource>> types, Pageable pageable);
+
+    @Query("""
+    SELECT r
+    FROM Resource r
+    JOIN r.tags t
+    WHERE r.class IN :typeList AND t.value IN :tags
+    GROUP BY r
+    HAVING COUNT(DISTINCT t.value) = :tagCount
+    """)
+    Page<Resource> findAllByTypesAndAllTags(
+            @Param("typeList") List<Class<? extends Resource>> typeList,
+            @Param("tags") String[] tags,
+            @Param("tagCount") long tagCount,
+            Pageable pageable
+    );
+
 }


Reply via email to