This is an automated email from the ASF dual-hosted git repository. ganning pushed a commit to branch resource-approval in repository https://gitbox.apache.org/repos/asf/airavata-portals.git
commit 34d41d03b082cc28bacf79b985edf61aa7b9e104 Author: ganning127 <[email protected]> AuthorDate: Sat Aug 9 23:34:25 2025 -0400 saving changes --- .../resources/RequestResourceVerification.tsx | 63 +++++ .../src/components/resources/ResourceDetails.tsx | 305 +++++++++++---------- .../src/components/resources/ResourceTypeBadge.tsx | 31 +-- .../components/resources/ResourceVerification.tsx | 75 +++++ .../src/interfaces/ResourceVerificationActivity.ts | 48 ++++ 5 files changed, 356 insertions(+), 166 deletions(-) diff --git a/airavata-research-portal/src/components/resources/RequestResourceVerification.tsx b/airavata-research-portal/src/components/resources/RequestResourceVerification.tsx new file mode 100644 index 000000000..1ae0348aa --- /dev/null +++ b/airavata-research-portal/src/components/resources/RequestResourceVerification.tsx @@ -0,0 +1,63 @@ +import { Resource } from "@/interfaces/ResourceType"; +import { Button, Dialog, useDialog, CloseButton } from "@chakra-ui/react"; +import { MdOutlineVerifiedUser } from "react-icons/md"; + +export const RequestResourceVerification = ({ + resource, + onRequestSubmitted, +}: { + resource: Resource; + onRequestSubmitted?: () => void; +}) => { + const dialog = useDialog(); + + const onSubmitForVerification = async () => { + console.log("Submitting resource for verification:", resource.id); + dialog.setOpen(false); + onRequestSubmitted?.(); + }; + + return ( + <> + <Dialog.RootProvider size="sm" value={dialog}> + <Dialog.Backdrop /> + <Dialog.Positioner> + <Dialog.Content> + <Dialog.Header> + <Dialog.Title>Resource Verification</Dialog.Title> + </Dialog.Header> + <Dialog.Body> + When you submit <b>{resource.name}</b> for verification, the + Airavata team will review it to ensure it meets the necessary + safety standards. This process may take some time, and you will be + notified once the verification is complete. + </Dialog.Body> + <Dialog.Footer> + <Button + width="100%" + colorPalette="yellow" + onClick={onSubmitForVerification} + > + Submit + </Button> + </Dialog.Footer> + <Dialog.CloseTrigger asChild> + <CloseButton size="sm" /> + </Dialog.CloseTrigger> + </Dialog.Content> + </Dialog.Positioner> + </Dialog.RootProvider> + + <Button + size="2xs" + colorPalette={"yellow"} + onClick={() => { + dialog.setOpen(true); + }} + > + <MdOutlineVerifiedUser /> + Request Verification + </Button> + </> + ); +}; diff --git a/airavata-research-portal/src/components/resources/ResourceDetails.tsx b/airavata-research-portal/src/components/resources/ResourceDetails.tsx index 7bd0b1766..0ea38ba0d 100644 --- a/airavata-research-portal/src/components/resources/ResourceDetails.tsx +++ b/airavata-research-portal/src/components/resources/ResourceDetails.tsx @@ -17,7 +17,7 @@ * under the License. */ -import {useLocation, useNavigate, useParams} from "react-router"; +import { useLocation, useNavigate, useParams } from "react-router"; import { Avatar, Badge, @@ -33,8 +33,8 @@ import { Spinner, Text, } from "@chakra-ui/react"; -import {useEffect, useState} from "react"; -import {BiArrowBack} from "react-icons/bi"; +import { useEffect, useState } from "react"; +import { BiArrowBack } from "react-icons/bi"; import api from "@/lib/api"; import { DatasetResource, @@ -43,20 +43,21 @@ import { RepositoryResource, Resource, } from "@/interfaces/ResourceType"; -import {Tag} from "@/interfaces/TagType"; -import {isValidImaage, resourceTypeToColor} from "@/lib/util"; -import {ResourceTypeBadge} from "./ResourceTypeBadge"; -import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum"; -import {ModelSpecificBox} from "../models/ModelSpecificBox"; -import {NotebookSpecificDetails} from "../notebooks/NotebookSpecificDetails"; -import {RepositorySpecificDetails} from "../repositories/RepositorySpecificDetails"; -import {CONTROLLER} from "@/lib/controller"; -import {DatasetSpecificDetails} from "../datasets/DatasetSpecificDetails"; -import {ResourceOptions} from "@/components/resources/ResourceOptions.tsx"; -import {toaster} from "@/components/ui/toaster.tsx"; -import {PrivacyEnum} from "@/interfaces/PrivacyEnum.ts"; -import {PrivateResourceTooltip} from "@/components/resources/PrivateResourceTooltip.tsx"; -import {useAuth} from "react-oidc-context"; +import { Tag } from "@/interfaces/TagType"; +import { isValidImaage, resourceTypeToColor } from "@/lib/util"; +import { ResourceTypeBadge } from "./ResourceTypeBadge"; +import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum"; +import { ModelSpecificBox } from "../models/ModelSpecificBox"; +import { NotebookSpecificDetails } from "../notebooks/NotebookSpecificDetails"; +import { RepositorySpecificDetails } from "../repositories/RepositorySpecificDetails"; +import { CONTROLLER } from "@/lib/controller"; +import { DatasetSpecificDetails } from "../datasets/DatasetSpecificDetails"; +import { ResourceOptions } from "@/components/resources/ResourceOptions.tsx"; +import { toaster } from "@/components/ui/toaster.tsx"; +import { PrivacyEnum } from "@/interfaces/PrivacyEnum.ts"; +import { PrivateResourceTooltip } from "@/components/resources/PrivateResourceTooltip.tsx"; +import { useAuth } from "react-oidc-context"; +import { ResourceVerification } from "./ResourceVerification"; async function getResource(id: string) { const response = await api.get(`${CONTROLLER.resources}/public/${id}`); @@ -64,11 +65,11 @@ async function getResource(id: string) { } const ResourceDetails = () => { - const {id} = useParams(); + const { id } = useParams(); const [resource, setResource] = useState<Resource | null>(null); const [loading, setLoading] = useState(false); const navigate = useNavigate(); - const {state} = useLocation(); + const { state } = useLocation(); const auth = useAuth(); useEffect(() => { if (!id || auth.isLoading) return; @@ -82,8 +83,8 @@ const ResourceDetails = () => { toaster.create({ title: "Resource not found", description: `id: ${id}`, - type: "error" - }) + type: "error", + }); } finally { setLoading(false); } @@ -94,9 +95,9 @@ const ResourceDetails = () => { if (loading) { return ( - <Center my={8}> - <Spinner/> - </Center> + <Center my={8}> + <Spinner /> + </Center> ); } else if (!resource) { return null; @@ -106,143 +107,147 @@ const ResourceDetails = () => { const goToResources = () => { navigate( - "/resources?resourceTypes=REPOSITORY%2CNOTEBOOK%2CDATASET%2CMODEL" - ) - } + "/resources?resourceTypes=REPOSITORY%2CNOTEBOOK%2CDATASET%2CMODEL" + ); + }; return ( - <> - <Container maxW="breakpoint-lg" mx="auto" p={4} mt={16}> - <Box> - <Button - variant="plain" - p={0} - onClick={goToResources} - > - <HStack - alignItems="center" - mb={4} - display="inline-flex" - _hover={{ - bg: "gray.300", - }} - p={1} - rounded="md" - > - <Icon> - <BiArrowBack/> - </Icon> - Back - </HStack> - </Button> - </Box> - - <HStack - alignItems={"flex-start"} + <> + <Container maxW="breakpoint-lg" mx="auto" p={4} mt={16}> + <Box> + <Button variant="plain" p={0} onClick={goToResources}> + <HStack + alignItems="center" mb={4} - gap={8} - justifyContent="space-between" - > - <Box w={'full'}> - <ResourceTypeBadge type={resource.type}/> - - <HStack mt={2} justifyContent={'space-between'} alignItems={'center'} flexWrap={'wrap'}> - <Heading as="h1" size="4xl"> - {resource.name} - </Heading> - - <HStack> - {resource.privacy === PrivacyEnum.PRIVATE && - <PrivateResourceTooltip/> - } - <ResourceOptions - resource={resource} - onDeleteSuccess={goToResources} - deleteable={true} - onUnStarSuccess={() => { - }} - /> - </HStack> - - </HStack> - - <HStack mt={2}> - {resource.tags.map((tag: Tag) => ( - <Badge - key={tag.id} - size="lg" - rounded="md" - colorPalette={resourceTypeToColor(resource.type)} - > - {tag.value} - </Badge> - ))} - </HStack> + display="inline-flex" + _hover={{ + bg: "gray.300", + }} + p={1} + rounded="md" + > + <Icon> + <BiArrowBack /> + </Icon> + Back + </HStack> + </Button> + </Box> + + <HStack + alignItems={"flex-start"} + mb={4} + gap={8} + justifyContent="space-between" + > + <Box w={"full"}> + <HStack gap={2} flexWrap="wrap"> + <ResourceTypeBadge type={resource.type} /> + <ResourceVerification + resource={resource} + setResource={setResource} + /> + </HStack> + <HStack + mt={1} + justifyContent={"space-between"} + alignItems={"center"} + flexWrap={"wrap"} + > + <Heading as="h1" size="4xl"> + {resource.name} + </Heading> - <HStack mt={8}> - {resource.authors.map((author: string) => { - return ( - <HStack key={author}> - <Avatar.Root shape="full" size="xl"> - <Avatar.Fallback name={author}/> - <Avatar.Image src={author}/> - </Avatar.Root> - - <Box> - <Text fontWeight="bold">{author}</Text> - </Box> - </HStack> - ); - })} + <HStack> + {resource.privacy === PrivacyEnum.PRIVATE && ( + <PrivateResourceTooltip /> + )} + <ResourceOptions + resource={resource} + onDeleteSuccess={goToResources} + deleteable={true} + onUnStarSuccess={() => {}} + /> </HStack> - </Box> - - <Box> - {validImage && ( - <Image - src={resource.headerImage} - alt="Notebook Header" - rounded="md" - maxW="300px" - /> - )} - </Box> - </HStack> - - <Separator my={6}/> - <Box> - <Heading fontWeight="bold" size="2xl"> - About - </Heading> + </HStack> - <Text>{resource.description}</Text> + <HStack mt={2}> + {resource.tags.map((tag: Tag) => ( + <Badge + key={tag.id} + size="lg" + rounded="md" + colorPalette={resourceTypeToColor(resource.type)} + > + {tag.value} + </Badge> + ))} + </HStack> + + <HStack mt={8}> + {resource.authors.map((author: string) => { + return ( + <HStack key={author}> + <Avatar.Root shape="full" size="xl"> + <Avatar.Fallback name={author} /> + <Avatar.Image src={author} /> + </Avatar.Root> + + <Box> + <Text fontWeight="bold">{author}</Text> + </Box> + </HStack> + ); + })} + </HStack> </Box> - <Separator my={8}/> - <Box> - {(resource.type as ResourceTypeEnum) === - ResourceTypeEnum.REPOSITORY && ( - <RepositorySpecificDetails - repository={resource as RepositoryResource} - /> - )} - - {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.DATASET && ( - <DatasetSpecificDetails dataset={resource as DatasetResource}/> - )} - - {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.MODEL && ( - <ModelSpecificBox model={resource as ModelResource}/> + {validImage && ( + <Image + src={resource.headerImage} + alt="Notebook Header" + rounded="md" + maxW="300px" + /> )} - - {(resource.type as ResourceTypeEnum) === - ResourceTypeEnum.NOTEBOOK && ( - <NotebookSpecificDetails notebook={resource as NotebookResource}/> - )} </Box> - </Container> - </> + </HStack> + + <Separator my={6} /> + <Box> + <Heading fontWeight="bold" size="2xl"> + About + </Heading> + + <Text>{resource.description}</Text> + </Box> + + <Separator my={8} /> + + <Box> + {(resource.type as ResourceTypeEnum) === + ResourceTypeEnum.REPOSITORY && ( + <RepositorySpecificDetails + repository={resource as RepositoryResource} + /> + )} + + {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.DATASET && ( + <DatasetSpecificDetails dataset={resource as DatasetResource} /> + )} + + {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.MODEL && ( + <ModelSpecificBox model={resource as ModelResource} /> + )} + + {(resource.type as ResourceTypeEnum) === + ResourceTypeEnum.NOTEBOOK && ( + <NotebookSpecificDetails notebook={resource as NotebookResource} /> + )} + </Box> + </Container> + </> ); }; diff --git a/airavata-research-portal/src/components/resources/ResourceTypeBadge.tsx b/airavata-research-portal/src/components/resources/ResourceTypeBadge.tsx index 9ca15e131..a334446e0 100644 --- a/airavata-research-portal/src/components/resources/ResourceTypeBadge.tsx +++ b/airavata-research-portal/src/components/resources/ResourceTypeBadge.tsx @@ -1,5 +1,5 @@ -import {resourceTypeToColor} from "@/lib/util"; -import {Badge} from "@chakra-ui/react"; +import { resourceTypeToColor } from "@/lib/util"; +import { Badge } from "@chakra-ui/react"; interface ResourceTypeBadgeProps { type: string; @@ -8,20 +8,19 @@ interface ResourceTypeBadgeProps { } export const ResourceTypeBadge = ({ - type, - ...props - }: ResourceTypeBadgeProps) => { + type, + ...props +}: ResourceTypeBadgeProps) => { return ( - <Badge - colorPalette={resourceTypeToColor(type)} - fontWeight="bold" - size="xs" - px="2" - py="1" - borderRadius="md" - {...props} - > - {type} - </Badge> + <Badge + colorPalette={resourceTypeToColor(type)} + fontWeight="bold" + px="2" + py="1" + borderRadius="md" + {...props} + > + {type} + </Badge> ); }; diff --git a/airavata-research-portal/src/components/resources/ResourceVerification.tsx b/airavata-research-portal/src/components/resources/ResourceVerification.tsx new file mode 100644 index 000000000..2c6536b40 --- /dev/null +++ b/airavata-research-portal/src/components/resources/ResourceVerification.tsx @@ -0,0 +1,75 @@ +import { Resource } from "@/interfaces/ResourceType"; +import { StatusEnum } from "@/interfaces/StatusEnum"; +import { isResourceOwner } from "@/lib/util"; +import { Badge } from "@chakra-ui/react"; +import { MdOutlineVerifiedUser } from "react-icons/md"; +import { useAuth } from "react-oidc-context"; +import { Tooltip } from "../ui/tooltip"; +import { RequestResourceVerification } from "./RequestResourceVerification"; +import { toaster } from "../ui/toaster"; + +export const ResourceVerification = ({ + resource, + setResource, +}: { + resource: Resource; + setResource: (resource: Resource) => void; +}) => { + const auth = useAuth(); + const isAuthor = isResourceOwner( + auth.user?.profile?.email || "INVALID", + resource + ); + + return ( + <> + {resource.status === StatusEnum.VERIFIED && ( + <Badge + colorPalette="green" + fontWeight="bold" + px="2" + py="1" + borderRadius="md" + > + <MdOutlineVerifiedUser /> + Verified + </Badge> + )} + + {isAuthor && resource.status === StatusEnum.PENDING && ( + <Tooltip content="The pending status is only visible to this resource's authors. It indicates that the resource is pending verification by the Airavata team."> + <Badge + colorPalette="yellow" + fontWeight="bold" + px="2" + py="1" + borderRadius="md" + > + <MdOutlineVerifiedUser /> + Pending Verification + </Badge> + </Tooltip> + )} + + {isAuthor && resource.status === StatusEnum.NONE && ( + <> + <RequestResourceVerification + resource={resource} + onRequestSubmitted={() => { + setResource({ + ...resource, + status: StatusEnum.PENDING, + } as Resource); + toaster.create({ + title: "Verification Requested", + description: + "Your request for resource verification has been submitted.", + type: "info", + }); + }} + /> + </> + )} + </> + ); +}; diff --git a/airavata-research-portal/src/interfaces/ResourceVerificationActivity.ts b/airavata-research-portal/src/interfaces/ResourceVerificationActivity.ts new file mode 100644 index 000000000..b5d366498 --- /dev/null +++ b/airavata-research-portal/src/interfaces/ResourceVerificationActivity.ts @@ -0,0 +1,48 @@ +import { Resource } from "./ResourceType"; +import { StatusEnum } from "./StatusEnum"; + +export interface ResourceVerificationActivity { + /** + * The unique identifier for the activity, generated as a UUID. + */ + id: string; + + /** + * The resource associated with this verification activity. + * Note: The @JsonBackReference in the backend means this property + * might be excluded from the JSON to prevent circular dependencies. + * If so, you might want to make this optional (e.g., `resource?: Resource`). + */ + resource?: Resource; + + /** + * The ID of the user who performed the activity (e.g., an admin or author). + */ + userId: string; + + /** + * The status of the verification activity. + */ + status: StatusEnum; + + /** + * An optional message associated with the activity, like a reason for rejection. + */ + message?: string; + + /** + * The timestamp when the record was created (ISO 8601 format). + */ + createdAt: string; + + /** + * The timestamp when the record was last updated (ISO 8601 format). + */ + updatedAt: string; +} + + + + + +
