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

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


The following commit(s) were added to refs/heads/cybershuttle-staging by this 
push:
     new 1de31a7352 Allow file tree exploring & handle rate limit error
1de31a7352 is described below

commit 1de31a735268b1d730b80c7e232eb89b4a80003e
Author: ganning127 <[email protected]>
AuthorDate: Tue Apr 1 13:45:33 2025 -0400

    Allow file tree exploring & handle rate limit error
---
 .../repositories/RepositorySpecificDetails.tsx     | 200 +++++++++++++++++++++
 .../src/components/resources/ResourceDetails.tsx   |   8 +-
 modules/research-framework/portal/src/lib/util.ts  |  10 ++
 3 files changed, 217 insertions(+), 1 deletion(-)

diff --git 
a/modules/research-framework/portal/src/components/repositories/RepositorySpecificDetails.tsx
 
b/modules/research-framework/portal/src/components/repositories/RepositorySpecificDetails.tsx
new file mode 100644
index 0000000000..2dd16cb69e
--- /dev/null
+++ 
b/modules/research-framework/portal/src/components/repositories/RepositorySpecificDetails.tsx
@@ -0,0 +1,200 @@
+import { RepositoryResource } from "@/interfaces/ResourceType";
+import { getGithubOwnerAndRepo } from "@/lib/util";
+import {
+  Box,
+  Icon,
+  ListItem,
+  ListRoot,
+  Spinner,
+  Button,
+  Text,
+  Breadcrumb,
+} from "@chakra-ui/react";
+import { Fragment, useEffect, useState } from "react";
+import { FiFolder, FiFile } from "react-icons/fi";
+
+interface FileTreeItem {
+  name: string;
+  type: string;
+  sha: string;
+  path: string;
+  size?: number;
+}
+
+export const RepositorySpecificDetails = ({
+  dataset,
+}: {
+  dataset: RepositoryResource;
+}) => {
+  const githubUrl = dataset.repositoryUrl;
+  const [fileTree, setFileTree] = useState<FileTreeItem[]>([]);
+  const [fileTreeLoading, setFileTreeLoading] = useState(false);
+  const [currentPath, setCurrentPath] = useState<string>("");
+  const [history, setHistory] = useState<string[]>([]);
+  const [fileContent, setFileContent] = useState<string | null>(null);
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const [fileName, setFileName] = useState<string | null>(null);
+  const [error, setError] = useState<string | null>(null);
+
+  useEffect(() => {
+    if (!githubUrl) return;
+    const ownerAndRepo = getGithubOwnerAndRepo(githubUrl);
+    if (ownerAndRepo) {
+      const owner = ownerAndRepo.owner;
+      const repo = ownerAndRepo.repo;
+      fetchFileTree(owner, repo, currentPath);
+    }
+  }, [githubUrl, currentPath]);
+
+  const fetchFileTree = (owner: string, repo: string, path: string) => {
+    setFileTreeLoading(true);
+    fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`)
+      .then((resp) => {
+        if (!resp.ok) {
+          setFileTree([]);
+          console.error("Error fetching file tree:", error);
+          setFileTreeLoading(false);
+          setError(
+            "Error fetching file tree. GitHub's API rate limit might be 
exceeded (60 / hour)."
+          );
+        }
+        return resp.json();
+      })
+      .then((data) => {
+        if (Array.isArray(data)) {
+          setFileTree(data);
+        } else {
+          setFileTree([]);
+        }
+        setFileTreeLoading(false);
+      });
+  };
+
+  const fetchFileContent = (owner: string, repo: string, path: string) => {
+    setFileTreeLoading(true);
+    fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`)
+      .then((resp) => {
+        if (!resp.ok) {
+          console.error("Error fetching file tree:", error);
+          setFileTreeLoading(false);
+          setFileContent(null);
+          setError(
+            "Error fetching file tree. GitHub's API rate limit might be 
exceeded (60 / hour)."
+          );
+        }
+        return resp.json();
+      })
+      .then((data) => {
+        if (data.content) {
+          setFileContent(atob(data.content.replace(/\n/g, "")));
+        }
+        setFileTreeLoading(false);
+      });
+  };
+
+  const handleFolderClick = (path: string) => {
+    setHistory((prev) => [...prev, currentPath]);
+    setCurrentPath(path);
+    setFileContent(null);
+  };
+
+  const handleFileClick = (path: string) => {
+    setHistory((prev) => [...prev, currentPath]);
+    const ownerAndRepo = getGithubOwnerAndRepo(githubUrl);
+    if (ownerAndRepo) {
+      const owner = ownerAndRepo.owner;
+      const repo = ownerAndRepo.repo;
+      fetchFileContent(owner, repo, path);
+      setFileName(path);
+    }
+
+    console.log("File clicked:", path);
+  };
+
+  const handleGoBack = () => {
+    if (history.length > 0) {
+      const previousPath = history[history.length - 1];
+      setHistory((prev) => prev.slice(0, -1));
+      setCurrentPath(previousPath);
+      setFileContent(null);
+      setFileName(null);
+    }
+  };
+
+  if (!githubUrl) return null;
+  if (fileTreeLoading) return <Spinner />;
+  if (error !== null) return null;
+
+  return (
+    <Box
+      bg="white"
+      p={4}
+      borderRadius="md"
+      shadow="md"
+      overflow="auto"
+      height="full"
+    >
+      <Button onClick={handleGoBack} mb={4}>
+        Back
+      </Button>
+      <Breadcrumb.Root mt={2}>
+        <Breadcrumb.List>
+          <Breadcrumb.Item>
+            <Breadcrumb.Link href="#">root</Breadcrumb.Link>
+          </Breadcrumb.Item>
+
+          {history.length > 0 &&
+            currentPath
+              .split("/")
+              .filter(Boolean) // Remove empty strings
+              .map((path, index) => (
+                <Fragment key={index}>
+                  <Breadcrumb.Separator />
+                  <Breadcrumb.Item>
+                    <Breadcrumb.Link href="#">{path}</Breadcrumb.Link>
+                  </Breadcrumb.Item>
+                </Fragment>
+              ))}
+        </Breadcrumb.List>
+      </Breadcrumb.Root>{" "}
+      {fileContent ? (
+        <Box p={4} bg="gray.100" borderRadius="md">
+          <Text whiteSpace="pre-wrap" fontSize="sm" fontFamily="monospace">
+            {fileContent}
+          </Text>
+        </Box>
+      ) : (
+        <ListRoot>
+          {Array.isArray(fileTree) &&
+            fileTree.map((file) => (
+              <ListItem
+                key={file.sha}
+                display="flex"
+                alignItems="center"
+                p={2}
+                borderRadius="md"
+                _hover={{ bg: "gray.100", cursor: "pointer" }}
+                onClick={() =>
+                  file.type === "dir"
+                    ? handleFolderClick(file.path)
+                    : handleFileClick(file.path)
+                }
+              >
+                <Icon
+                  as={file.type === "dir" ? FiFolder : FiFile}
+                  color={file.type === "dir" ? "blue.500" : "gray.500"}
+                  mr={2}
+                />
+                <p>{file.name}</p>
+                {file.size !== undefined && file.size > 0 && (
+                  <Text fontSize="xs" color="gray.500" ml={2}>
+                    ({file.size} bytes)
+                  </Text>
+                )}
+              </ListItem>
+            ))}
+        </ListRoot>
+      )}
+    </Box>
+  );
+};
diff --git 
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
 
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
index 9703047ba3..0841b6dd3b 100644
--- 
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
+++ 
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
@@ -19,6 +19,7 @@ import api from "@/lib/api";
 import {
   ModelResource,
   NotebookResource,
+  RepositoryResource,
   Resource,
 } from "@/interfaces/ResourceType";
 import { Tag } from "@/interfaces/TagType";
@@ -28,6 +29,7 @@ import { ResourceTypeBadge } from "./ResourceTypeBadge";
 import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
 import { ModelSpecificBox } from "../models/ModelSpecificBox";
 import { NotebookSpecificDetails } from "../notebooks/NotebookSpecificDetails";
+import { RepositorySpecificDetails } from 
"../repositories/RepositorySpecificDetails";
 
 async function getResource(id: string) {
   const response = await api.get(`/project-management/resources/${id}`);
@@ -148,7 +150,11 @@ const ResourceDetails = () => {
 
         <Box>
           {(resource.type as ResourceTypeEnum) ===
-            ResourceTypeEnum.REPOSITORY && <Text>REPO only</Text>}
+            ResourceTypeEnum.REPOSITORY && (
+            <RepositorySpecificDetails
+              dataset={resource as RepositoryResource}
+            />
+          )}
 
           {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.MODEL && (
             <ModelSpecificBox model={resource as ModelResource} />
diff --git a/modules/research-framework/portal/src/lib/util.ts 
b/modules/research-framework/portal/src/lib/util.ts
index 86fdb5c4ce..bad255b39a 100644
--- a/modules/research-framework/portal/src/lib/util.ts
+++ b/modules/research-framework/portal/src/lib/util.ts
@@ -19,4 +19,14 @@ export const isValidImaage = (url: string) => {
     return true;
   }
   return false
+}
+
+export const getGithubOwnerAndRepo = (url: string) => {
+  const match = url.match(/github\.com\/([^/]+)\/([^/]+)/);
+  if (match) {
+    const owner = match[1];
+    const repo = match[2].replace(/\.git$/, "");
+    return { owner, repo };
+  }
+  return null;
 }
\ No newline at end of file

Reply via email to