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