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;
+}
+
+
+
+
+
+

Reply via email to