This is an automated email from the ASF dual-hosted git repository.
yasith pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata.git
The following commit(s) were added to refs/heads/master by this push:
new 6eac42a4e7 Support liking resources (#527)
6eac42a4e7 is described below
commit 6eac42a4e71325dd4c40d0baf6d919553a9dbbe6
Author: Ganning Xu <[email protected]>
AuthorDate: Sat Jun 21 05:04:36 2025 -0700
Support liking resources (#527)
* support liking resources
* spotless
* spotless
* better loading animation
* add license header
* like -> star
* spotless
* use contructor based dependency injection
---
modules/research-framework/portal/src/App.tsx | 130 ++++++++-------
.../portal/src/components/add/DatasetSearch.tsx | 2 +-
.../portal/src/components/add/RepoSearch.tsx | 23 ++-
.../portal/src/components/home/ResourceCard.tsx | 36 +++-
.../projects/AssociatedProjectsSection.tsx | 4 +-
.../components/resources/DeleteResourceButton.tsx | 59 ++++---
.../src/components/resources/ResourceDetails.tsx | 32 +++-
.../src/components/resources/ResourceOptions.tsx | 88 ++++++++++
.../components/resources/StarResourceButton.tsx | 103 ++++++++++++
.../components/resources/StarredResourcesPage.tsx | 74 +++++++++
.../portal/src/components/resources/index.tsx | 4 +-
.../portal/src/layouts/NavBar.tsx | 184 ++++++++++++---------
.../portal/src/lib/controller.ts | 19 +++
.../research/service/config/AuthzTokenFilter.java | 2 +-
.../service/controller/ResourceController.java | 45 +++--
.../research/service/handlers/ResourceHandler.java | 53 +++++-
.../service/model/entity/ResourceStar.java | 70 ++++++++
.../service/model/repo/ResourceStarRepository.java | 39 +++++
18 files changed, 780 insertions(+), 187 deletions(-)
diff --git a/modules/research-framework/portal/src/App.tsx
b/modules/research-framework/portal/src/App.tsx
index 18d7036162..4df2957806 100644
--- a/modules/research-framework/portal/src/App.tsx
+++ b/modules/research-framework/portal/src/App.tsx
@@ -1,30 +1,47 @@
-import { useColorMode } from "./components/ui/color-mode";
-import { Route, Routes, useLocation, useNavigate } from "react-router";
+/*
+ * 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 {useColorMode} from "./components/ui/color-mode";
+import {Route, Routes, useLocation, useNavigate} from "react-router";
import Home from "./components/home";
-import { Models } from "./components/models";
-import { Datasets } from "./components/datasets";
+import {Models} from "./components/models";
+import {Datasets} from "./components/datasets";
import ResourceDetails from "./components/resources/ResourceDetails";
import Notebooks from "./components/notebooks";
import Repositories from "./components/repositories";
-import { Login } from "./components/auth/UserLoginPage";
+import {Login} from "./components/auth/UserLoginPage";
import ProtectedComponent from "./components/auth/ProtectedComponent";
-import { AuthProvider, AuthProviderProps } from "react-oidc-context";
-import { useEffect, useState } from "react";
+import {AuthProvider, AuthProviderProps} from "react-oidc-context";
+import {useEffect, useState} from "react";
import NavBarFooterLayout from "./layouts/NavBarFooterLayout";
-import { CybershuttleLanding } from "./components/home/CybershuttleLanding";
-import {
- APP_REDIRECT_URI,
- CLIENT_ID,
- OPENID_CONFIG_URL,
-} from "./lib/constants";
-import { WebStorageStateStore } from "oidc-client-ts";
-import { Resources } from "./components/resources";
-import { UserSet } from "./components/auth/UserSet";
-import { Toaster } from "./components/ui/toaster";
-import { Events } from "./components/events";
-import { AddRepoMaster } from "./components/add/AddRepoMaster";
-import { Add } from "./components/add";
-import { AddProjectMaster } from "./components/add/AddProjectMaster";
+import {CybershuttleLanding} from "./components/home/CybershuttleLanding";
+import {APP_REDIRECT_URI, CLIENT_ID, OPENID_CONFIG_URL,} from
"./lib/constants";
+import {WebStorageStateStore} from "oidc-client-ts";
+import {Resources} from "./components/resources";
+import {UserSet} from "./components/auth/UserSet";
+import {Toaster} from "./components/ui/toaster";
+import {Events} from "./components/events";
+import {AddRepoMaster} from "./components/add/AddRepoMaster";
+import {Add} from "./components/add";
+import {AddProjectMaster} from "./components/add/AddProjectMaster";
+import {StarredResourcesPage} from
"@/components/resources/StarredResourcesPage.tsx";
+
function App() {
const colorMode = useColorMode();
const navigate = useNavigate();
@@ -57,7 +74,7 @@ function App() {
userinfo_endpoint: data.userinfo_endpoint,
jwks_uri: data.jwks_uri,
},
- userStore: new WebStorageStateStore({ store: window.localStorage }),
+ userStore: new WebStorageStateStore({store: window.localStorage}),
automaticSilentRenew: true,
};
@@ -75,41 +92,42 @@ function App() {
}
return (
- <>
- <AuthProvider
- {...oidcConfig}
- onSigninCallback={() => {
- navigate(location.search, { replace: true });
- }}
- >
- <Toaster />
- <UserSet />
- <Routes>
- {/* Public Route */}
- <Route element={<NavBarFooterLayout />}>
- <Route path="/" element={<CybershuttleLanding />} />
- <Route path="/login" element={<Login />} />
- <Route path="/resources" element={<Resources />} />
- <Route path="/events" element={<Events />} />
- <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>
+ <>
+ <AuthProvider
+ {...oidcConfig}
+ onSigninCallback={() => {
+ navigate(location.search, {replace: true});
+ }}
+ >
+ <Toaster/>
+ <UserSet/>
+ <Routes>
+ {/* Public Route */}
+ <Route element={<NavBarFooterLayout/>}>
+ <Route path="/" element={<CybershuttleLanding/>}/>
+ <Route path="/login" element={<Login/>}/>
+ <Route path="/resources" element={<Resources/>}/>
+ <Route path="/events" element={<Events/>}/>
+ <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 */}
- <Route
- element={<ProtectedComponent Component={NavBarFooterLayout} />}
- >
- <Route path="/sessions" element={<Home />} />
- <Route path="/add" element={<Add />} />
- <Route path="/add/repo" element={<AddRepoMaster />} />
- <Route path="/add/project" element={<AddProjectMaster />} />
- </Route>
- </Routes>
- </AuthProvider>
- </>
+ {/* Protected Routes with Layout */}
+ <Route
+ element={<ProtectedComponent Component={NavBarFooterLayout}/>}
+ >
+ <Route path="/resources/starred"
element={<StarredResourcesPage/>}/>
+ <Route path="/sessions" element={<Home/>}/>
+ <Route path="/add" element={<Add/>}/>
+ <Route path="/add/repo" element={<AddRepoMaster/>}/>
+ <Route path="/add/project" element={<AddProjectMaster/>}/>
+ </Route>
+ </Routes>
+ </AuthProvider>
+ </>
);
}
diff --git
a/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
b/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
index 9df9f97bbc..915f241890 100644
--- a/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
+++ b/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
@@ -39,7 +39,7 @@ export const DatasetSearchInput = ({
const timeout = setTimeout(async () => {
try {
- const response = await api.get(`${CONTROLLER.resources}/search`, {
+ const response = await
api.get(`${CONTROLLER.resources}/public/search`, {
params: {
type: ResourceTypeEnum.DATASET,
name: datasetSearch,
diff --git
a/modules/research-framework/portal/src/components/add/RepoSearch.tsx
b/modules/research-framework/portal/src/components/add/RepoSearch.tsx
index 3f588a5d14..cc230b0d12 100644
--- a/modules/research-framework/portal/src/components/add/RepoSearch.tsx
+++ b/modules/research-framework/portal/src/components/add/RepoSearch.tsx
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
@@ -41,7 +60,7 @@ const RepoSearchInput = ({
const timeout = setTimeout(async () => {
try {
- const response = await api.get(`${CONTROLLER.resources}/search`, {
+ const response = await
api.get(`${CONTROLLER.resources}/public/search`, {
params: {
type: ResourceTypeEnum.REPOSITORY,
name: repoSearch,
@@ -85,7 +104,7 @@ const RepoSearchInput = ({
</HStack>
</Field.Root>
- <ResourceCard size="sm" resource={selectedRepo} deletable={false}/>
+ <ResourceCard size="sm" resource={selectedRepo} deletable={false}
removeOnUnStar={false}/>
</>
);
}
diff --git
a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
index 047ab3aa5e..6846a19098 100644
--- a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
+++ b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
@@ -1,3 +1,22 @@
+/*
+ * 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 {ModelResource, Resource} from "@/interfaces/ResourceType";
import {Tag} from "@/interfaces/TagType";
import {isValidImaage, resourceTypeToColor} from "@/lib/util";
@@ -5,18 +24,20 @@ import {Avatar, Badge, Box, Card, HStack, Image, Text,} from
"@chakra-ui/react";
import {ResourceTypeBadge} from "../resources/ResourceTypeBadge";
import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
import {ModelCardButton} from "../models/ModelCardButton";
-import {DeleteResourceButton} from
"@/components/resources/DeleteResourceButton.tsx";
import {useState} from "react";
import {Link} from 'react-router';
+import {ResourceOptions} from "@/components/resources/ResourceOptions.tsx";
export const ResourceCard = ({
resource,
size = "sm",
- deletable = true
+ deletable = true,
+ removeOnUnStar = false,
}: {
resource: Resource;
size?: "sm" | "md" | "lg";
- deletable?: boolean
+ deletable?: boolean;
+ removeOnUnStar?: boolean;
}) => {
const [hideCard, setHideCard] = useState(false);
const author = resource.authors[0];
@@ -28,10 +49,14 @@ export const ResourceCard = ({
const linkToWithType = `${resource.type}/${resource.id}`;
const link = '/resources/' + linkToWithType;
- const onDeleteSuccess = () => {
+ const hideCardCallback = () => {
setHideCard(true);
}
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const dummyOnUnStarSuccess = (_: string) => {
+ }
+
const content = (
<Card.Root
overflow="hidden"
@@ -62,7 +87,8 @@ export const ResourceCard = ({
<Card.Header>
<HStack justifyContent={'space-between'} alignItems={'center'}
flexWrap={'wrap'}>
<Card.Title>{resource.name}</Card.Title>
- {deletable && <DeleteResourceButton resource={resource}
onSuccess={onDeleteSuccess}/>}
+ <ResourceOptions deleteable={deletable} resource={resource}
onDeleteSuccess={hideCardCallback}
+ onUnStarSuccess={removeOnUnStar ?
hideCardCallback : dummyOnUnStarSuccess}/>
</HStack>
</Card.Header>
diff --git
a/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
b/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
index ff8606952e..56b137fab1 100644
---
a/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
+++
b/modules/research-framework/portal/src/components/projects/AssociatedProjectsSection.tsx
@@ -7,7 +7,7 @@ import {ProjectCard} from "../home/ProjectCard";
async function fetchProjects(id: string) {
try {
- const resp = await api.get(`${CONTROLLER.resources}/${id}/projects`);
+ const resp = await
api.get(`${CONTROLLER.resources}/public/${id}/projects`);
return resp.data;
} catch (error) {
console.error("Error fetching projects:", error);
@@ -38,7 +38,7 @@ export const AssociatedProjectsSection = ({
<Heading fontWeight="bold" size="2xl" mb={2}>
Associated Projects
</Heading>
-
+
<SimpleGrid columns={{base: 1, md: 2}} gap={2}>
{projects.map((project) => (
<ProjectCard key={project.id} project={project}/>
diff --git
a/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
b/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
index 580c4f5051..d4d5ba83bd 100644
---
a/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
+++
b/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
@@ -1,6 +1,23 @@
-import {Box, Button, CloseButton, Dialog, Input, Menu, Portal, Text,
useDialog} from "@chakra-ui/react";
-import {BsThreeDots} from "react-icons/bs";
-import {FaTrash} from "react-icons/fa";
+/*
+ * 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 {Box, Button, CloseButton, Dialog, Input, Portal, Text, useDialog} from
"@chakra-ui/react";
import {Resource} from "@/interfaces/ResourceType.ts";
import {useAuth} from "react-oidc-context";
import {isResourceOwner} from "@/lib/util.ts";
@@ -8,6 +25,8 @@ import {useState} from "react";
import api from "@/lib/api.ts";
import {CONTROLLER} from "@/lib/controller.ts";
import {toaster} from "@/components/ui/toaster.tsx";
+import {FaTrash} from "react-icons/fa";
+import {ResourceOptionButton} from
"@/components/resources/ResourceOptions.tsx";
export const DeleteResourceButton = ({
resource,
@@ -48,27 +67,21 @@ export const DeleteResourceButton = ({
return (
<>
- <Menu.Root>
- <Menu.Trigger _hover={{
- cursor: 'pointer',
- }}>
- <BsThreeDots/>
- </Menu.Trigger>
- <Menu.Positioner>
- <Menu.Content>
- <Menu.Item value={"delete"}
- color="fg.error"
- _hover={{bg: "bg.error", color: "fg.error", cursor:
"pointer"}}
- onClick={() => dialog.setOpen(true)}
- >
- <FaTrash/>
- <Box flex="1">Delete</Box>
-
- </Menu.Item>
+ <ResourceOptionButton
+ gap={2}
+ color={'red.600'}
+ onClick={() => {
+ dialog.setOpen(true)
+ }}
+ _hover={{
+ cursor: 'pointer',
+ bg: 'red.200',
+ }}
+ >
+ <FaTrash/>
+ <Box>Delete</Box>
+ </ResourceOptionButton>
- </Menu.Content>
- </Menu.Positioner>
- </Menu.Root>
<Dialog.RootProvider size="sm" value={dialog}>
<Portal>
diff --git
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
index 80c5b17712..ae94b7bdd6 100644
---
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
+++
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
@@ -1,3 +1,22 @@
+/*
+ * 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 {useLocation, useNavigate, useParams} from "react-router";
import {
Avatar,
@@ -32,10 +51,10 @@ import {NotebookSpecificDetails} from
"../notebooks/NotebookSpecificDetails";
import {RepositorySpecificDetails} from
"../repositories/RepositorySpecificDetails";
import {CONTROLLER} from "@/lib/controller";
import {DatasetSpecificDetails} from "../datasets/DatasetSpecificDetails";
-import {DeleteResourceButton} from
"@/components/resources/DeleteResourceButton.tsx";
+import {ResourceOptions} from "@/components/resources/ResourceOptions.tsx";
async function getResource(id: string) {
- const response = await api.get(`${CONTROLLER.resources}/${id}`);
+ const response = await api.get(`${CONTROLLER.resources}/public/${id}`);
return response.data;
}
@@ -110,7 +129,14 @@ const ResourceDetails = () => {
{resource.name}
</Heading>
- <DeleteResourceButton resource={resource}
onSuccess={goToResources}/>
+ <ResourceOptions
+ resource={resource}
+ onDeleteSuccess={goToResources}
+ deleteable={true}
+ onUnStarSuccess={() => {
+ }}
+ />
+
</HStack>
<HStack mt={2}>
diff --git
a/modules/research-framework/portal/src/components/resources/ResourceOptions.tsx
b/modules/research-framework/portal/src/components/resources/ResourceOptions.tsx
new file mode 100644
index 0000000000..7fc54aa2fd
--- /dev/null
+++
b/modules/research-framework/portal/src/components/resources/ResourceOptions.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 {Resource} from "@/interfaces/ResourceType.ts";
+import {Button, ButtonProps, Menu, VStack} from "@chakra-ui/react";
+import {BsThreeDots} from "react-icons/bs";
+import {DeleteResourceButton} from
"@/components/resources/DeleteResourceButton.tsx";
+import {StarResourceButton} from
"@/components/resources/StarResourceButton.tsx";
+import {useAuth} from "react-oidc-context";
+
+export const ResourceOptions = ({resource, deleteable = true, onDeleteSuccess,
onUnStarSuccess}:
+ {
+ resource: Resource,
+ deleteable: boolean,
+ onDeleteSuccess: () => void,
+ onUnStarSuccess: (resourceId: string) => void
+ }) => {
+
+ const auth = useAuth();
+ if (!auth.isAuthenticated) {
+ return null;
+ }
+
+ return (
+ <>
+ <Menu.Root>
+ <Menu.Trigger _hover={{
+ cursor: 'pointer',
+ }}>
+ <BsThreeDots/>
+ </Menu.Trigger>
+ <Menu.Positioner>
+ <Menu.Content>
+ <VStack gap={2} alignItems={'start'}>
+ <StarResourceButton resource={resource}
onSuccess={onUnStarSuccess}/>
+ {deleteable && <DeleteResourceButton resource={resource}
onSuccess={onDeleteSuccess}/>}
+ </VStack>
+ </Menu.Content>
+ </Menu.Positioner>
+ </Menu.Root>
+ </>
+ )
+}
+
+
+type ResourceOptionButtonProps = {
+ onClick: () => void;
+ children?: React.ReactNode;
+} & ButtonProps;
+
+export const ResourceOptionButton = ({
+ onClick,
+ children,
+ ...rest
+ }: ResourceOptionButtonProps) => {
+ return (
+ <Button
+ w="full"
+ transition="all .2s"
+ rounded="md"
+ gap={2}
+ onClick={onClick}
+ p={0}
+ display={'flex'}
+ justifyContent={'flex-start'}
+ bg={'transparent'}
+ {...rest}
+ >
+ {children}
+ </Button>
+ );
+};
diff --git
a/modules/research-framework/portal/src/components/resources/StarResourceButton.tsx
b/modules/research-framework/portal/src/components/resources/StarResourceButton.tsx
new file mode 100644
index 0000000000..9e45b492f8
--- /dev/null
+++
b/modules/research-framework/portal/src/components/resources/StarResourceButton.tsx
@@ -0,0 +1,103 @@
+/*
+ * 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 {Resource} from "@/interfaces/ResourceType.ts";
+import {ResourceOptionButton} from
"@/components/resources/ResourceOptions.tsx";
+import {Box} from "@chakra-ui/react";
+import api from "@/lib/api.ts";
+import {CONTROLLER} from "@/lib/controller.ts";
+import {toaster} from "@/components/ui/toaster.tsx";
+import {useEffect, useState} from "react";
+import {BsStar, BsStarFill} from "react-icons/bs";
+
+export const StarResourceButton = ({
+ resource,
+ onSuccess,
+ }: {
+ resource: Resource,
+ onSuccess: (resourceId: string) => void,
+}) => {
+ const [changeStarLoading, setChangeStarLoading] = useState(false);
+ const [initialLoad, setinitialLoad] = useState(false);
+ const [starred, setStarred] = useState(false);
+
+ useEffect(() => {
+ async function getWhetherUserStarred() {
+ setinitialLoad(true);
+ const resp = await
api.get(`${CONTROLLER.resources}/${resource.id}/star`);
+ setStarred(resp.data);
+ setinitialLoad(false);
+ }
+
+ getWhetherUserStarred();
+ }, []);
+
+ const handleStarResource = async () => {
+ try {
+ setChangeStarLoading(true);
+ await api.post(`${CONTROLLER.resources}/${resource.id}/star`);
+ toaster.create({
+ title: starred ? "Unstarred" : "Starred",
+ description: resource.name,
+ type: "success",
+ })
+ setStarred(prev => {
+ if (prev) {
+ onSuccess(resource.id || "INVALID");
+ }
+ return !prev;
+ });
+ } catch {
+ toaster.create({
+ title: "Error liking resource",
+ type: "error",
+ });
+ } finally {
+ setChangeStarLoading(false);
+ }
+ }
+
+ if (initialLoad) {
+ return null;
+ }
+
+ return (
+ <>
+ <ResourceOptionButton
+ gap={2}
+ _hover={{
+ cursor: 'pointer',
+ bg: 'blue.200',
+ }}
+ color={'black'}
+ onClick={handleStarResource}
+ loading={changeStarLoading}
+ >
+ {
+ starred ? <BsStarFill color={'#EFBF04'}/> : <BsStar/>
+ }
+ <Box>
+ {
+ starred ? "Unstar" : "Star"
+ }
+ </Box>
+ </ResourceOptionButton>
+ </>
+ )
+}
\ No newline at end of file
diff --git
a/modules/research-framework/portal/src/components/resources/StarredResourcesPage.tsx
b/modules/research-framework/portal/src/components/resources/StarredResourcesPage.tsx
new file mode 100644
index 0000000000..a81fd7987b
--- /dev/null
+++
b/modules/research-framework/portal/src/components/resources/StarredResourcesPage.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 {useAuth} from "react-oidc-context";
+import api from "@/lib/api.ts";
+import {CONTROLLER} from "@/lib/controller.ts";
+import {Container, SimpleGrid, Spinner} from "@chakra-ui/react";
+import {Resource} from "@/interfaces/ResourceType.ts";
+import {ResourceCard} from "@/components/home/ResourceCard.tsx";
+import {PageHeader} from "@/components/PageHeader.tsx";
+
+export const StarredResourcesPage = () => {
+ const [starredResources, setStarredResources] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const auth = useAuth();
+
+ useEffect(() => {
+ if (auth.isLoading) return;
+
+ async function getStarredResources() {
+ setLoading(true);
+ const resp = await
api.get(`${CONTROLLER.resources}/${auth.user?.profile.email}/stars`)
+ setStarredResources(resp.data);
+ setLoading(false);
+ }
+
+ getStarredResources();
+ }, [auth.isLoading]);
+
+ return (
+ <Container maxW="container.lg" mt={8}>
+ <PageHeader title={"Starred Resources"}
+ description={"Resources that you have starred will show up
here, for easy access."}/>
+ <SimpleGrid
+ columns={{base: 1, md: 2, lg: 4}}
+ mt={4}
+ gap={2}
+ justifyContent="space-around"
+ >
+ {starredResources.map((resource: Resource) => {
+ return (
+ <ResourceCard
+ resource={resource}
+ key={resource.id}
+ removeOnUnStar={true}
+ />
+ );
+ })}
+ </SimpleGrid>
+ {
+ loading && (
+ <Spinner/>
+ )
+ }
+ </Container>
+ )
+}
\ 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
index a86bd1b626..74f064deac 100644
--- a/modules/research-framework/portal/src/components/resources/index.tsx
+++ b/modules/research-framework/portal/src/components/resources/index.tsx
@@ -29,7 +29,7 @@ const getResources = async (
stringTagsArr: string[],
searchText: string
) => {
- const response = await api.get(`${CONTROLLER.resources}/`, {
+ const response = await api.get(`${CONTROLLER.resources}/public`, {
params: {
type: types.join(","),
tag: stringTagsArr.join(","),
@@ -43,7 +43,7 @@ const getResources = async (
const getTags = async () => {
try {
- const response = await api.get(`${CONTROLLER.resources}/tags/all`);
+ const response = await api.get(`${CONTROLLER.resources}/public/tags/all`);
return response.data;
} catch (error) {
console.error("Error fetching:", error);
diff --git a/modules/research-framework/portal/src/layouts/NavBar.tsx
b/modules/research-framework/portal/src/layouts/NavBar.tsx
index 7be0495c0d..ea53c19967 100644
--- a/modules/research-framework/portal/src/layouts/NavBar.tsx
+++ b/modules/research-framework/portal/src/layouts/NavBar.tsx
@@ -1,23 +1,42 @@
+/*
+ * 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 {
- Text,
+ Box,
+ Button,
+ ButtonProps,
+ Collapsible,
Flex,
- Spacer,
- Image,
HStack,
- Box,
IconButton,
- useDisclosure,
- Collapsible,
- Button,
+ Image,
+ Spacer,
Stack,
- ButtonProps,
+ Text,
+ useDisclosure,
} from "@chakra-ui/react";
import ApacheAiravataLogo from "../assets/airavata-logo.png";
-import { Link, useNavigate } from "react-router";
-import { RxHamburgerMenu } from "react-icons/rx";
-import { IoClose } from "react-icons/io5";
-import { UserMenu } from "@/components/auth/UserMenu";
-import { useAuth } from "react-oidc-context";
+import {Link, useNavigate} from "react-router";
+import {RxHamburgerMenu} from "react-icons/rx";
+import {IoClose} from "react-icons/io5";
+import {UserMenu} from "@/components/auth/UserMenu";
+import {useAuth} from "react-oidc-context";
const NAV_CONTENT = [
{
@@ -35,6 +54,11 @@ const NAV_CONTENT = [
url: "/add",
needsAuth: true,
},
+ {
+ title: "Starred",
+ url: "/resources/starred",
+ needsAuth: true,
+ },
{
title: "Events",
url: "/events",
@@ -64,7 +88,7 @@ interface NavLinkProps extends ButtonProps {
}
const NavBar = () => {
- const { open, onToggle } = useDisclosure();
+ const {open, onToggle} = useDisclosure();
const navigate = useNavigate();
const auth = useAuth();
@@ -75,80 +99,80 @@ const NavBar = () => {
return true; // Show all items that do not require authentication
});
- const NavLink = ({ title, url, ...props }: NavLinkProps) => (
- <Button
- variant="plain"
- px={2}
- _hover={{ bg: "gray.200" }}
- onClick={() => {
- navigate(url);
- onToggle();
- }}
- {...props}
- >
- <Text color="gray.700" fontSize="md" textAlign="left">
- {title}
- </Text>
- </Button>
+ const NavLink = ({title, url, ...props}: NavLinkProps) => (
+ <Button
+ variant="plain"
+ px={2}
+ _hover={{bg: "gray.200"}}
+ onClick={() => {
+ navigate(url);
+ onToggle();
+ }}
+ {...props}
+ >
+ <Text color="gray.700" fontSize="md" textAlign="left">
+ {title}
+ </Text>
+ </Button>
);
return (
- <Box position="sticky" top="0" zIndex="1000" bg="white" boxShadow="sm">
- <Flex align="center" p={4}>
- {/* Hamburger Menu (Mobile Only) */}
- <IconButton
- aria-label="Toggle Navigation"
- display={{ base: "inline-flex", md: "none" }}
- onClick={onToggle}
- variant="ghost"
- mr={2}
- >
- {open ? <IoClose size={24} /> : <RxHamburgerMenu size={24} />}
- </IconButton>
+ <Box position="sticky" top="0" zIndex="1000" bg="white" boxShadow="sm">
+ <Flex align="center" p={4}>
+ {/* Hamburger Menu (Mobile Only) */}
+ <IconButton
+ aria-label="Toggle Navigation"
+ display={{base: "inline-flex", md: "none"}}
+ onClick={onToggle}
+ variant="ghost"
+ mr={2}
+ >
+ {open ? <IoClose size={24}/> : <RxHamburgerMenu size={24}/>}
+ </IconButton>
- {/* Logo */}
- <Link to="/">
- <Image src={ApacheAiravataLogo} alt="Logo" boxSize="30px" />
- </Link>
+ {/* Logo */}
+ <Link to="/">
+ <Image src={ApacheAiravataLogo} alt="Logo" boxSize="30px"/>
+ </Link>
- {/* Desktop Nav Links */}
- <HStack ml={4} display={{ base: "none", md: "flex" }}>
- {filteredNavContent.map((item) => (
- <NavLink key={item.title} title={item.title} url={item.url} />
- ))}
- </HStack>
+ {/* Desktop Nav Links */}
+ <HStack ml={4} display={{base: "none", md: "flex"}}>
+ {filteredNavContent.map((item) => (
+ <NavLink key={item.title} title={item.title} url={item.url}/>
+ ))}
+ </HStack>
- <Spacer />
+ <Spacer/>
- {/* User Profile */}
- <UserMenu />
- </Flex>
+ {/* User Profile */}
+ <UserMenu/>
+ </Flex>
- {/* Mobile Nav Links (Collapse) */}
- <Collapsible.Root open={open}>
- <Collapsible.Content>
- <Stack
- direction="column"
- bg="white"
- px={4}
- pb={4}
- spaceY={2}
- display={{ md: "none" }}
- >
- {filteredNavContent.map((item) => (
- <Box key={item.title} w="100%">
- <NavLink
- key={item.title}
- title={item.title}
- url={item.url}
- width="100%"
- />
- </Box>
- ))}
- </Stack>
- </Collapsible.Content>
- </Collapsible.Root>
- </Box>
+ {/* Mobile Nav Links (Collapse) */}
+ <Collapsible.Root open={open}>
+ <Collapsible.Content>
+ <Stack
+ direction="column"
+ bg="white"
+ px={4}
+ pb={4}
+ spaceY={2}
+ display={{md: "none"}}
+ >
+ {filteredNavContent.map((item) => (
+ <Box key={item.title} w="100%">
+ <NavLink
+ key={item.title}
+ title={item.title}
+ url={item.url}
+ width="100%"
+ />
+ </Box>
+ ))}
+ </Stack>
+ </Collapsible.Content>
+ </Collapsible.Root>
+ </Box>
);
};
diff --git a/modules/research-framework/portal/src/lib/controller.ts
b/modules/research-framework/portal/src/lib/controller.ts
index 866e82e630..d4657b1877 100644
--- a/modules/research-framework/portal/src/lib/controller.ts
+++ b/modules/research-framework/portal/src/lib/controller.ts
@@ -1,3 +1,22 @@
+/*
+ * 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.
+ */
+
export const CONTROLLER = {
projects: "/projects",
hub: "/hub",
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 0fcd374cf5..5b459f8405 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
@@ -70,7 +70,7 @@ public class AuthzTokenFilter extends OncePerRequestFilter {
|| path.startsWith("/swagger-ui")
|| path.startsWith("/swagger-resources")
|| path.startsWith("/webjars/")
- || path.startsWith("/api/v1/rf/resources");
+ || path.startsWith("/api/v1/rf/resources/public");
}
@Override
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 d5f2f696a1..d20de6bb65 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
@@ -37,7 +37,6 @@ import
org.apache.airavata.research.service.model.entity.RepositoryResource;
import org.apache.airavata.research.service.model.entity.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
@@ -57,11 +56,13 @@ public class ResourceController {
private static final Logger LOGGER =
LoggerFactory.getLogger(ResourceController.class);
- @org.springframework.beans.factory.annotation.Autowired
- private ResourceHandler resourceHandler;
+ private final ResourceHandler resourceHandler;
+ private final ProjectHandler projectHandler;
- @Autowired
- private ProjectHandler projectHandler;
+ public ResourceController(ResourceHandler resourceHandler, ProjectHandler
projectHandler) {
+ this.resourceHandler = resourceHandler;
+ this.projectHandler = projectHandler;
+ }
@PostMapping("/dataset")
public ResponseEntity<ResourceResponse> createDatasetResource(@RequestBody
DatasetResource datasetResource) {
@@ -96,13 +97,13 @@ public class ResourceController {
}
@Operation(summary = "Get all tags")
- @GetMapping(value = "/tags/all")
+ @GetMapping(value = "/public/tags/all")
public
ResponseEntity<List<org.apache.airavata.research.service.model.entity.Tag>>
getTags() {
return ResponseEntity.ok(resourceHandler.getAllTagsByPopularity());
}
@Operation(summary = "Get dataset, notebook, repository, or model")
- @GetMapping(value = "/{id}")
+ @GetMapping(value = "/public/{id}")
public ResponseEntity<Resource> getResource(@PathVariable(value = "id")
String id) {
return ResponseEntity.ok(resourceHandler.getResourceById(id));
}
@@ -114,7 +115,7 @@ public class ResourceController {
}
@Operation(summary = "Get all resources")
- @GetMapping("/")
+ @GetMapping("/public")
public ResponseEntity<Page<Resource>> getAllResources(
@RequestParam(value = "pageNumber", defaultValue = "0") int
pageNumber,
@RequestParam(value = "pageSize", defaultValue = "10") int
pageSize,
@@ -139,7 +140,7 @@ public class ResourceController {
}
@Operation(summary = "Get resource by name")
- @GetMapping("/search")
+ @GetMapping("/public/search")
public ResponseEntity<List<Resource>> searchResource(
@RequestParam(value = "type") ResourceTypeEnum type,
@RequestParam(value = "name", required = false) String name) {
@@ -149,7 +150,7 @@ public class ResourceController {
}
@Operation(summary = "Get projects associated with a resource")
- @GetMapping(value = "/{id}/projects")
+ @GetMapping(value = "/public/{id}/projects")
public ResponseEntity<List<Project>>
getProjectsFromResourceId(@PathVariable(value = "id") String id) {
Resource resouce = resourceHandler.getResourceById(id);
List<Project> projects;
@@ -165,6 +166,30 @@ public class ResourceController {
return ResponseEntity.ok(projects);
}
+ @Operation(summary = "Star/unstar a resource")
+ @PostMapping(value = "/{id}/star")
+ public ResponseEntity<Boolean> starOrUnstarResource(@PathVariable(value =
"id") String id) {
+ return ResponseEntity.ok(resourceHandler.starOrUnstarResource(id));
+ }
+
+ @Operation(summary = "Check whether a user star-ed a resource")
+ @GetMapping(value = "/{id}/star")
+ public ResponseEntity<Boolean>
checkWhetherUserStarredResource(@PathVariable(value = "id") String id) {
+ return
ResponseEntity.ok(resourceHandler.checkWhetherUserStarredResource(id));
+ }
+
+ @Operation(summary = "Get resource star count")
+ @GetMapping(value = "/resources/{id}/count")
+ public ResponseEntity<Long> getResourceStarCount(@PathVariable(value =
"id") String id) {
+ return ResponseEntity.ok(resourceHandler.getResourceStarCount(id));
+ }
+
+ @Operation(summary = "Get all starred resources of a user")
+ @GetMapping(value = "/{userId}/stars")
+ public ResponseEntity<List<Resource>>
getAllStarredResources(@PathVariable(value = "userId") String id) {
+ return ResponseEntity.ok(resourceHandler.getAllStarredResources(id));
+ }
+
private Class<? extends Resource> getResourceType(ResourceTypeEnum
resourceTypeEnum) {
return switch (resourceTypeEnum) {
case REPOSITORY -> RepositoryResource.class;
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 475e2b7c18..0e84a2b36f 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
@@ -36,9 +36,11 @@ import org.apache.airavata.research.service.enums.StatusEnum;
import org.apache.airavata.research.service.model.UserContext;
import org.apache.airavata.research.service.model.entity.RepositoryResource;
import org.apache.airavata.research.service.model.entity.Resource;
+import org.apache.airavata.research.service.model.entity.ResourceStar;
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.ResourceStarRepository;
import org.apache.airavata.research.service.model.repo.TagRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,16 +58,19 @@ public class ResourceHandler {
private final TagRepository tagRepository;
private final ResourceRepository resourceRepository;
private final ProjectRepository projectRepository;
+ private final ResourceStarRepository resourceStarRepository;
public ResourceHandler(
AiravataService airavataService,
TagRepository tagRepository,
ResourceRepository resourceRepository,
- ProjectRepository projectRepository) {
+ ProjectRepository projectRepository,
+ ResourceStarRepository resourceStarRepository) {
this.airavataService = airavataService;
this.tagRepository = tagRepository;
this.resourceRepository = resourceRepository;
this.projectRepository = projectRepository;
+ this.resourceStarRepository = resourceStarRepository;
}
public void initializeResource(Resource resource) {
@@ -174,6 +179,50 @@ public class ResourceHandler {
return resource;
}
+ public boolean starOrUnstarResource(String resourceId) {
+ Resource resource = getResourceById(resourceId);
+ String userId = UserContext.userId();
+
+ List<ResourceStar> resourceStars =
resourceStarRepository.findByResourceAndUserId(resource, userId);
+
+ if (resourceStars.isEmpty()) {
+ // user has not starred the resource yet
+ ResourceStar resourceStar = new ResourceStar();
+ resourceStar.setUserId(userId);
+ resourceStar.setResource(resource);
+ resourceStarRepository.save(resourceStar);
+ } else {
+ ResourceStar resourceStar = resourceStars.get(0);
+ resourceStarRepository.delete(resourceStar);
+ }
+
+ return true;
+ }
+
+ public boolean checkWhetherUserStarredResource(String resourceId) {
+ Resource resource = getResourceById(resourceId);
+ String userId = UserContext.userId();
+
+ return resourceStarRepository.existsByResourceAndUserId(resource,
userId);
+ }
+
+ public List<Resource> getAllStarredResources(String userId) {
+ String loggedInUser = UserContext.userId();
+ if (!loggedInUser.equals(userId)) {
+ throw new RuntimeException(
+ String.format("User %s is not authorized to request stars
for %s", loggedInUser, userId));
+ }
+
+ List<ResourceStar> resourceStars =
+
resourceStarRepository.findByUserIdAndResourceState(loggedInUser,
StateEnum.ACTIVE);
+ return
resourceStars.stream().map(ResourceStar::getResource).collect(Collectors.toList());
+ }
+
+ public long getResourceStarCount(String resourceId) {
+ Resource resource = getResourceById(resourceId);
+ return resourceStarRepository.countResourceStarByResource(resource);
+ }
+
public Resource getResourceById(String id) {
// Your logic to fetch the resource by ID
Optional<Resource> opResource =
resourceRepository.findByIdAndState(id, StateEnum.ACTIVE);
@@ -204,7 +253,7 @@ public class ResourceHandler {
}
resource.setState(StateEnum.DELETED);
- resourceRepository.delete(resource);
+ resourceRepository.save(resource);
return true;
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceStar.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceStar.java
new file mode 100644
index 0000000000..e7ec4d7fc4
--- /dev/null
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/ResourceStar.java
@@ -0,0 +1,70 @@
+/**
+*
+* 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.
+*/
+package org.apache.airavata.research.service.model.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import org.hibernate.annotations.UuidGenerator;
+
+@Entity(name = "RESOURCE_STAR")
+public class ResourceStar {
+
+ @Id
+ @GeneratedValue
+ @UuidGenerator
+ @Column(nullable = false, updatable = false, length = 48)
+ private String id;
+
+ @Column(name = "user_id", nullable = false)
+ private String userId;
+
+ @ManyToOne(optional = false, fetch = FetchType.EAGER)
+ @JoinColumn(name = "resource_id", nullable = false)
+ private Resource resource;
+
+ public Resource getResource() {
+ return resource;
+ }
+
+ public void setResource(Resource resource) {
+ this.resource = resource;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceStarRepository.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceStarRepository.java
new file mode 100644
index 0000000000..267cdd9404
--- /dev/null
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceStarRepository.java
@@ -0,0 +1,39 @@
+/**
+*
+* 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.
+*/
+package org.apache.airavata.research.service.model.repo;
+
+import java.util.List;
+import org.apache.airavata.research.service.enums.StateEnum;
+import org.apache.airavata.research.service.model.entity.Resource;
+import org.apache.airavata.research.service.model.entity.ResourceStar;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface ResourceStarRepository extends JpaRepository<ResourceStar,
String> {
+
+ boolean existsByResourceAndUserId(Resource resource, String ownerId);
+
+ List<ResourceStar> findByResourceAndUserId(Resource resource, String
ownerId);
+
+ List<ResourceStar> findByUserIdAndResourceState(String loggedInUser,
StateEnum stateEnum);
+
+ long countResourceStarByResource(Resource resource);
+}