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 cd8d67670f Authentication for Research Portal (#481)
cd8d67670f is described below
commit cd8d67670f926cdc0f16bca0adb32b79f25c409f
Author: Ganning Xu <[email protected]>
AuthorDate: Fri Apr 4 00:17:08 2025 -0400
Authentication for Research Portal (#481)
* Basic auth
* Broken authzfilter authorization
* sections
* pushing...
* application.yml change
* prep for deployment
* change app url
* Full support for frontend + backend
* Change dev user and app url
---
modules/research-framework/portal/src/App.tsx | 20 +--
.../portal/src/components/PageHeader.tsx | 4 +-
.../portal/src/components/auth/login.tsx | 70 ++++-----
.../portal/src/components/datasets/index.tsx | 12 +-
.../portal/src/components/home/ProjectCard.tsx | 51 +------
.../portal/src/components/home/ProjectsSection.tsx | 23 +--
.../portal/src/components/home/ResourceCard.tsx | 12 +-
.../portal/src/components/home/SessionCard.tsx | 57 +++----
.../src/components/home/SessionCardControls.tsx | 88 +++++++++++
.../portal/src/components/home/SessionsSection.tsx | 56 +++++--
.../home/StartSessionFromProjectButton.tsx | 126 ++++++++++++++++
.../portal/src/components/home/index.tsx | 2 +
.../portal/src/components/models/index.tsx | 11 +-
.../src/components/notebooks/ProjectDetails.tsx | 163 ---------------------
.../portal/src/components/notebooks/index.tsx | 12 +-
.../src/components/repositories/RepositoryCard.tsx | 39 -----
.../repositories/RepositorySpecificDetails.tsx | 4 -
.../portal/src/components/repositories/index.tsx | 12 +-
.../src/components/resources/ResourceDetails.tsx | 26 ++--
.../portal/src/components/ui/toaster.tsx | 43 ++++++
.../portal/src/interfaces/ResourceType.ts | 4 +-
modules/research-framework/portal/src/lib/api.ts | 8 +-
.../research-framework/portal/src/lib/constants.ts | 10 +-
.../portal/src/lib/controller.ts | 6 +
.../service/ResponseTypes/RedirectResponse.java | 17 +++
.../service/controller/ProjectController.java | 28 ++++
.../service/controller/ResearchHubController.java | 51 ++-----
.../service/controller/ResourceController.java | 9 +-
.../service/controller/SessionController.java | 50 +++++++
.../research/service/handlers/ProjectHandler.java | 6 +
.../service/handlers/ResearchHubHandler.java | 4 -
.../research/service/handlers/SessionHandler.java | 7 +-
.../service/model/repo/SessionRepository.java | 7 +
.../src/main/resources/application.yml | 3 +-
34 files changed, 582 insertions(+), 459 deletions(-)
diff --git a/modules/research-framework/portal/src/App.tsx
b/modules/research-framework/portal/src/App.tsx
index d8927464ca..42206ac062 100644
--- a/modules/research-framework/portal/src/App.tsx
+++ b/modules/research-framework/portal/src/App.tsx
@@ -6,7 +6,7 @@ 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/login";
+import { Login } from "./components/auth/Login";
import ProtectedComponent from "./components/auth/ProtectedComponent";
import { useAuth } from "react-oidc-context";
import { useEffect } from "react";
@@ -18,9 +18,10 @@ function App() {
}
const user = useAuth();
+
useEffect(() => {
if (user.isAuthenticated) {
- setUserProvider(() => user.user);
+ setUserProvider(() => Promise.resolve(user.user ?? null));
}
}, [user]);
@@ -58,21 +59,6 @@ function App() {
element={<ProtectedComponent Component={ResourceDetails} />}
/>
</Route>
-
- {/* <Route path="/notebook">
- <Route path=":slug" element={<ProjectDetails />} />
- </Route>
- <Route path="/repository">
- <Route path=":slug" element={<ProjectDetails />} />
- </Route>
- <Route path="/models">
- <Route index element={<Models />} />
- <Route path=":id" element={<ModelDetails />} />
- </Route>
- <Route path="/datasets">
- <Route index element={<Datasets />} />
- <Route path=":slug" element={<DatasetDetails />} />
- </Route> */}
</Routes>
</BrowserRouter>
</>
diff --git a/modules/research-framework/portal/src/components/PageHeader.tsx
b/modules/research-framework/portal/src/components/PageHeader.tsx
index f4e1e8ee85..f145d0994d 100644
--- a/modules/research-framework/portal/src/components/PageHeader.tsx
+++ b/modules/research-framework/portal/src/components/PageHeader.tsx
@@ -1,5 +1,5 @@
import { Box, HStack, Text, Heading, Icon } from "@chakra-ui/react";
-import { ReactNode } from "react";
+import { ElementType } from "react";
export const PageHeader = ({
title,
@@ -7,7 +7,7 @@ export const PageHeader = ({
description,
}: {
title: string;
- icon?: ReactNode;
+ icon?: ElementType;
description: string;
}) => {
const fontSize = "4xl";
diff --git a/modules/research-framework/portal/src/components/auth/login.tsx
b/modules/research-framework/portal/src/components/auth/login.tsx
index 9dc3a1c88b..545fb3eeff 100644
--- a/modules/research-framework/portal/src/components/auth/login.tsx
+++ b/modules/research-framework/portal/src/components/auth/login.tsx
@@ -1,46 +1,40 @@
-import { Box, Center, SimpleGrid, Flex, Text, Button } from "@chakra-ui/react";
+import { Center, Text, Button, Image, VStack, Stack } from "@chakra-ui/react";
import { useAuth } from "react-oidc-context";
+import AirvataLogo from "../../assets/airavata-logo.png";
export const Login = () => {
const auth = useAuth();
return (
- <>
- <Center height="100vh">
- <SimpleGrid
- columns={{
- base: 1,
- lg: 2,
- }}
- gap={32}
- alignItems="center"
- >
- <Box>
- <Flex gap={4} alignItems="center">
- <Box>
- <Text fontWeight="bold">Airavata Data Catalog</Text>
- <Text color="gray.500" fontSize="sm">
- Login to access the data catalog
- </Text>
- </Box>
- </Flex>
+ <Center height="100vh">
+ <Stack
+ gap={8}
+ alignItems="center"
+ padding={4}
+ direction={{ base: "column", lg: "row" }}
+ >
+ <Image src={AirvataLogo} alt="Airavata Logo" maxWidth="100px" />
+ <VStack gap={1} alignItems="flex-start">
+ <Text fontWeight="bold" fontSize="2xl">
+ Cybershuttle Data Catalog
+ </Text>
+ <Text color="gray.500" fontSize="sm" maxWidth="300px">
+ Jupyter Notebooks, repositories, datasets, and models, for
+ scientists, by scientists.
+ </Text>
- <Button
- bg="black"
- color="white"
- _hover={{ bg: "gray.800" }}
- _active={{ bg: "gray.900" }}
- rounded="full"
- w="300px"
- onClick={() => {
- console.log("Sign in clicked");
- auth.signinRedirect();
- }}
- >
- Institution Login
- </Button>
- </Box>
- </SimpleGrid>
- </Center>
- </>
+ <Button
+ colorPalette="black"
+ _active={{ bg: "gray.900" }}
+ w="300px"
+ onClick={() => {
+ console.log("Sign in clicked");
+ auth.signinRedirect();
+ }}
+ >
+ Institution Login
+ </Button>
+ </VStack>
+ </Stack>
+ </Center>
);
};
diff --git
a/modules/research-framework/portal/src/components/datasets/index.tsx
b/modules/research-framework/portal/src/components/datasets/index.tsx
index 6d89984f6d..690dd61152 100644
--- a/modules/research-framework/portal/src/components/datasets/index.tsx
+++ b/modules/research-framework/portal/src/components/datasets/index.tsx
@@ -10,12 +10,18 @@ import { DatasetResource } from "@/interfaces/ResourceType";
import { useEffect, useState } from "react";
import { ResourceCard } from "../home/ResourceCard";
import { resourceTypeToColor } from "@/lib/util";
+import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
+import { CONTROLLER } from "@/lib/controller";
const getDatasets = async () => {
try {
- const response = await api.get(
- "/project-management/resources?type=DATASET"
- );
+ const response = await api.get(`${CONTROLLER.resources}/`, {
+ params: {
+ type: ResourceTypeEnum.DATASET,
+ pageNumber: 0,
+ pageSize: 100,
+ },
+ });
const data = response.data;
return data;
} catch (error) {
diff --git
a/modules/research-framework/portal/src/components/home/ProjectCard.tsx
b/modules/research-framework/portal/src/components/home/ProjectCard.tsx
index 8096c93a4a..d11ce5c968 100644
--- a/modules/research-framework/portal/src/components/home/ProjectCard.tsx
+++ b/modules/research-framework/portal/src/components/home/ProjectCard.tsx
@@ -1,38 +1,10 @@
import { ProjectType } from "@/interfaces/ProjectType";
-import api from "@/lib/api";
-import { API_VERSION, BACKEND_URL } from "@/lib/constants";
-import { Button, Card, HStack, Text } from "@chakra-ui/react";
-import { useEffect, useState } from "react";
-
-async function checkIfSessionWithProjectExists(project: ProjectType) {
- try {
- await api.get(`/hub/project/${project.id}/exists`);
-
- return true;
- } catch (error: any) {
- return false;
- }
-}
+import { Card, HStack, Text } from "@chakra-ui/react";
+import { StartSessionFromProjectButton } from
"./StartSessionFromProjectButton";
export const ProjectCard = ({ project }: { project: ProjectType }) => {
- const [exists, setExists] = useState(false);
- useEffect(() => {
- let intervalId: NodeJS.Timeout;
-
- async function fetchData() {
- const exists = await checkIfSessionWithProjectExists(project);
- setExists(exists);
- }
-
- fetchData(); // Initial fetch
-
- intervalId = setInterval(fetchData, 2000); // Fetch every 5 seconds
-
- return () => clearInterval(intervalId); // Cleanup on unmount
- }, [project]); // Depend on project so it updates correctly
-
return (
- <Card.Root overflow="hidden" size="md">
+ <Card.Root overflow="hidden" size="md" height="fit-content">
<Card.Body gap="2">
{/* Card Content */}
<HStack alignItems="center" justifyContent="space-between">
@@ -55,22 +27,7 @@ export const ProjectCard = ({ project }: { project:
ProjectType }) => {
</Text>
</Text>
- {exists ? (
- <Text color="green.600">Session already started</Text>
- ) : (
- <Button
- colorPalette="black"
- size="sm"
- onClick={() => {
- window.open(
-
`${BACKEND_URL}/api/${API_VERSION}/rf/hub/project/${project.id}?sessionName=${project.name}`,
- "_blank"
- );
- }}
- >
- Open Project
- </Button>
- )}
+ <StartSessionFromProjectButton project={project} />
</Card.Body>
</Card.Root>
);
diff --git
a/modules/research-framework/portal/src/components/home/ProjectsSection.tsx
b/modules/research-framework/portal/src/components/home/ProjectsSection.tsx
index ad8f930465..062cc3a171 100644
--- a/modules/research-framework/portal/src/components/home/ProjectsSection.tsx
+++ b/modules/research-framework/portal/src/components/home/ProjectsSection.tsx
@@ -3,19 +3,22 @@ import api from "@/lib/api";
import { SimpleGrid } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { ProjectCard } from "./ProjectCard";
+import { CONTROLLER } from "@/lib/controller";
-async function getAllProjects() {
- try {
- const response = await api.get("/hub/projects");
- const data = response.data;
- return data;
- } catch (error) {
- console.error("Error fetching:", error);
- }
-}
export const ProjectsSection = () => {
const [projects, setProjects] = useState<ProjectType[]>([]);
+ async function getAllProjects() {
+ try {
+ const response = await api.get(`${CONTROLLER.projects}/`);
+ const data = response.data;
+ console.log("projects", data);
+ return data;
+ } catch (error) {
+ console.error("Error fetching:", error);
+ }
+ }
+
useEffect(() => {
async function init() {
const projects = await getAllProjects();
@@ -26,7 +29,7 @@ export const ProjectsSection = () => {
init();
}, []);
return (
- <SimpleGrid mt={2} columns={{ base: 1, md: 2, lg: 2 }}>
+ <SimpleGrid mt={2} columns={{ base: 1, md: 2, lg: 2 }} gap={4}>
{projects.map((project: ProjectType) => {
return <ProjectCard project={project} key={project.id} />;
})}
diff --git
a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
index b5bc8c26e8..bceb4bdd92 100644
--- a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
+++ b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
@@ -16,8 +16,8 @@ import { ResourceTypeEnum } from
"@/interfaces/ResourceTypeEnum";
import { ModelCardButton } from "../models/ModelCardButton";
export const ResourceCard = ({ resource }: { resource: Resource }) => {
- console.log("Resource:", resource);
const author = resource.authors[0];
+ console.log("author" + author);
const isValidImage = isValidImaage(resource.headerImage);
@@ -82,16 +82,12 @@ export const ResourceCard = ({ resource }: { resource:
Resource }) => {
{author && (
<HStack>
<Avatar.Root shape="full" size="sm">
- <Avatar.Fallback
- name={author.firstName + " " + author.lastName}
- />
- <Avatar.Image src={author.avatar} />
+ <Avatar.Fallback name={author} />
+ <Avatar.Image src={author} />
</Avatar.Root>
<Box>
- <Text fontWeight="bold">
- {author.firstName + " " + author.lastName}
- </Text>
+ <Text fontWeight="bold">{author}</Text>
</Box>
</HStack>
)}
diff --git
a/modules/research-framework/portal/src/components/home/SessionCard.tsx
b/modules/research-framework/portal/src/components/home/SessionCard.tsx
index a529a0cde8..6c7637ce47 100644
--- a/modules/research-framework/portal/src/components/home/SessionCard.tsx
+++ b/modules/research-framework/portal/src/components/home/SessionCard.tsx
@@ -1,30 +1,32 @@
import { SessionType } from "@/interfaces/SessionType";
-import { API_VERSION, BACKEND_URL } from "@/lib/constants";
-import {
- Box,
- Text,
- Card,
- Heading,
- HStack,
- Badge,
- Button,
-} from "@chakra-ui/react";
+import { Box, Text, Card, Heading, HStack, Badge } from "@chakra-ui/react";
+import { SessionCardControls } from "./SessionCardControls";
+import { SessionStatusEnum } from "@/interfaces/SessionStatusEnum";
const getColorPalette = (status: string) => {
switch (status) {
- case "CREATED":
+ case SessionStatusEnum.CREATED:
return "green";
- case "stopped":
+ case SessionStatusEnum.RUNNING:
+ return "blue";
+ case SessionStatusEnum.FINISHED:
+ return "purple";
+ case SessionStatusEnum.TERMINATED:
+ return "gray";
+ case SessionStatusEnum.ERROR:
return "red";
- case "pending":
- return "yellow";
default:
return "gray";
}
};
+
+const PREVENT_CONTROL_STATUS = [
+ SessionStatusEnum.TERMINATED,
+ SessionStatusEnum.ERROR,
+];
export const SessionCard = ({ session }: { session: SessionType }) => {
return (
- <Card.Root size="sm">
+ <Card.Root size="sm" height="fit-content">
<Card.Header>
<HStack justify="space-between" alignItems="flex-start">
<Box>
@@ -43,28 +45,9 @@ export const SessionCard = ({ session }: { session:
SessionType }) => {
: {new Date(session.createdAt).toLocaleString()}
</Text>
- <HStack alignItems="center" mt={2}>
- <Button
- size="sm"
- colorPalette="red"
- variant="subtle"
- onClick={() => {}}
- >
- Terminate
- </Button>
- <Button
- size="sm"
- colorPalette="green"
- onClick={() => {
- window.open(
-
`${BACKEND_URL}/api/${API_VERSION}/rf/hub/sessions/${session.id}/resolve`,
- "_blank"
- );
- }}
- >
- Open Session
- </Button>
- </HStack>
+ {!PREVENT_CONTROL_STATUS.includes(session.status) && (
+ <SessionCardControls session={session} />
+ )}
</Card.Body>
</Card.Root>
);
diff --git
a/modules/research-framework/portal/src/components/home/SessionCardControls.tsx
b/modules/research-framework/portal/src/components/home/SessionCardControls.tsx
new file mode 100644
index 0000000000..d7dc22be89
--- /dev/null
+++
b/modules/research-framework/portal/src/components/home/SessionCardControls.tsx
@@ -0,0 +1,88 @@
+import { SessionType } from "@/interfaces/SessionType";
+import { HStack, Button } from "@chakra-ui/react";
+import api from "@/lib/api";
+import { CONTROLLER } from "@/lib/controller";
+import { toaster } from "../ui/toaster";
+import { useState } from "react";
+import { SessionStatusEnum } from "@/interfaces/SessionStatusEnum";
+
+export const SessionCardControls = ({ session }: { session: SessionType }) => {
+ const [openLoading, setOpenLoading] = useState(false);
+ const [terminateLoading, setTerminateLoading] = useState(false);
+ const [hideControls, setHideControls] = useState(false);
+
+ const handleOpenSession = async () => {
+ setOpenLoading(true);
+ try {
+ const resp = await api.get(
+ `${CONTROLLER.hub}/resume/session/${session.id}`
+ );
+ const data = resp.data;
+ window.open(data.redirectUrl, "_blank");
+ toaster.create({
+ title: "Session started",
+ description: session.sessionName,
+ type: "success",
+ });
+ } catch {
+ toaster.create({
+ title: "Error resuming session",
+ type: "error",
+ });
+ }
+ setOpenLoading(false);
+ };
+
+ const handleTerminateSession = async () => {
+ setTerminateLoading(true);
+ try {
+ // empty request body
+ await api.patch(
+ `${CONTROLLER.sessions}/${session.id}`,
+ {},
+ {
+ params: {
+ status: SessionStatusEnum.TERMINATED,
+ },
+ }
+ );
+
+ toaster.create({
+ title: "Session terminated",
+ description: session.sessionName,
+ type: "success",
+ });
+ setHideControls(true);
+ } catch {
+ toaster.create({
+ title: "Error terminating session",
+ type: "error",
+ });
+ }
+ setTerminateLoading(false);
+ };
+
+ return (
+ <HStack alignItems="center" mt={2} hidden={hideControls}>
+ <Button
+ size="sm"
+ colorPalette="red"
+ variant="subtle"
+ onClick={handleTerminateSession}
+ loading={terminateLoading}
+ disabled={terminateLoading}
+ >
+ Terminate
+ </Button>
+ <Button
+ size="sm"
+ colorPalette="green"
+ onClick={handleOpenSession}
+ loading={openLoading}
+ disabled={openLoading}
+ >
+ Open Session
+ </Button>
+ </HStack>
+ );
+};
diff --git
a/modules/research-framework/portal/src/components/home/SessionsSection.tsx
b/modules/research-framework/portal/src/components/home/SessionsSection.tsx
index 20e8e63f44..41f230c3d5 100644
--- a/modules/research-framework/portal/src/components/home/SessionsSection.tsx
+++ b/modules/research-framework/portal/src/components/home/SessionsSection.tsx
@@ -1,12 +1,21 @@
import { SessionType } from "@/interfaces/SessionType";
import { SessionCard } from "./SessionCard";
-import { GridContainer } from "../GridContainer";
import api from "@/lib/api";
import { useEffect, useState } from "react";
+import { CONTROLLER } from "@/lib/controller";
+import { Button, HStack, SimpleGrid } from "@chakra-ui/react";
+import { SessionStatusEnum } from "@/interfaces/SessionStatusEnum";
-async function getSessions() {
+async function getSessions(status: SessionStatusEnum | null = null) {
try {
- const response = await api.get("/hub/sessions");
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const params = {} as any;
+ if (status) {
+ params.status = status;
+ }
+ const response = await api.get(`${CONTROLLER.sessions}/`, {
+ params,
+ });
const data = response.data;
return data;
} catch (error) {
@@ -15,31 +24,56 @@ async function getSessions() {
}
export const SessionsSection = () => {
+ const [sessionStatusFilter, setSessionStatusFilter] =
+ useState<SessionStatusEnum | null>(null);
const [sessions, setSessions] = useState<SessionType[]>([]);
useEffect(() => {
let intervalId: NodeJS.Timeout;
async function fetchData() {
- const sessions = await getSessions();
- setSessions(sessions);
+ const allSessions = await getSessions(sessionStatusFilter);
+
+ setSessions(allSessions);
}
fetchData(); // Initial fetch
-
// eslint-disable-next-line prefer-const
- intervalId = setInterval(fetchData, 2000); // Fetch every 2 seconds
+ intervalId = setInterval(fetchData, 2000); // Refetch every 2s
- return () => clearInterval(intervalId); // Cleanup on unmount
- }, []);
+ return () => clearInterval(intervalId); // Cleanup
+ }, [sessionStatusFilter]); // <== Important dependency
return (
<>
- <GridContainer>
+ <HStack flexWrap={"wrap"} gap={2} mt={4}>
+ <Button
+ variant={sessionStatusFilter === null ? "solid" : "outline"}
+ size="sm"
+ onClick={() => {
+ setSessionStatusFilter(null);
+ }}
+ >
+ All
+ </Button>
+ {Object.values(SessionStatusEnum).map((status) => (
+ <Button
+ key={status}
+ variant={sessionStatusFilter === status ? "solid" : "outline"}
+ size="sm"
+ onClick={() => {
+ setSessionStatusFilter(status);
+ }}
+ >
+ {status}
+ </Button>
+ ))}
+ </HStack>
+ <SimpleGrid mt={4} columns={{ base: 1, md: 2, lg: 3 }} gap={4}>
{sessions.map((session: SessionType) => {
return <SessionCard key={session.id} session={session} />;
})}
- </GridContainer>
+ </SimpleGrid>
</>
);
};
diff --git
a/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
b/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
new file mode 100644
index 0000000000..db0bdb7723
--- /dev/null
+++
b/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
@@ -0,0 +1,126 @@
+import { ProjectType } from "@/interfaces/ProjectType";
+import api from "@/lib/api";
+import { CONTROLLER } from "@/lib/controller";
+import {
+ useDialog,
+ Dialog,
+ Button,
+ Portal,
+ Fieldset,
+ Input,
+ CloseButton,
+} from "@chakra-ui/react";
+import { useState } from "react";
+import { Toaster, toaster } from "../ui/toaster";
+
+export const StartSessionFromProjectButton = ({
+ project,
+}: {
+ project: ProjectType;
+}) => {
+ const dialog = useDialog();
+ const defaultName =
+ new Date().toLocaleDateString() + " " + project.name + " Session";
+ const [sessionName, setSessionName] = useState<string>(defaultName);
+ const [loadingOpenProject, setLoadingOpenProject] = useState(false);
+
+ const handleOpenProject = async () => {
+ if (!sessionName) {
+ toaster.create({
+ title: "Session name is required",
+ type: "error",
+ });
+ return;
+ }
+ setLoadingOpenProject(true);
+ try {
+ const resp = await api.get(
+ `${CONTROLLER.hub}/start/project/${project.id}`,
+ {
+ params: {
+ sessionName: sessionName,
+ },
+ }
+ );
+ const data = resp.data;
+ window.open(data.redirectUrl, "_blank");
+ dialog.setOpen(false);
+ toaster.create({
+ title: "Session started",
+ type: "success",
+ });
+ } catch (error) {
+ console.error("Error fetching project:", error);
+ toaster.create({
+ title: "Error starting session",
+ type: "error",
+ });
+ }
+ setLoadingOpenProject(false);
+ };
+
+ return (
+ <>
+ <Toaster />
+
+ <Dialog.RootProvider
+ value={dialog}
+ size="sm"
+ lazyMount={true}
+ unmountOnExit={true}
+ onExitComplete={() => {
+ setSessionName(defaultName); // Reset session name when modal closes
+ }}
+ >
+ <Dialog.Trigger asChild>
+ <Button colorPalette="black" size="sm">
+ Start Project Session
+ </Button>
+ </Dialog.Trigger>
+
+ <Portal>
+ <Dialog.Backdrop />
+ <Dialog.Positioner>
+ <Dialog.Content>
+ <Dialog.Body p="4">
+ <Dialog.Title>New Session</Dialog.Title>
+ <Dialog.Description mt="2">
+ Project: <b>{project.name}</b>
+ </Dialog.Description>
+
+ <Fieldset.Root size="lg" mt={2}>
+ <Fieldset.Legend>Session Name</Fieldset.Legend>
+ <Input
+ placeholder="My new session"
+ mt={2}
+ value={sessionName}
+ onChange={(e) => setSessionName(e.target.value)}
+ />
+ <Fieldset.HelperText mt="2">
+ All session and session names are private and cannot be
seen
+ by anyone else.
+ </Fieldset.HelperText>
+ </Fieldset.Root>
+
+ <Button
+ colorPalette="black"
+ width="100%"
+ type="submit"
+ mt={4}
+ onClick={handleOpenProject}
+ disabled={!sessionName || loadingOpenProject}
+ loading={loadingOpenProject}
+ >
+ Create session
+ </Button>
+ </Dialog.Body>
+ <Dialog.CloseTrigger asChild>
+ <CloseButton _hover={{ bg: "gray.100" }} size="sm" />
+ </Dialog.CloseTrigger>
+ </Dialog.Content>
+ </Dialog.Positioner>
+ </Portal>
+ </Dialog.RootProvider>
+ </>
+ );
+};
diff --git a/modules/research-framework/portal/src/components/home/index.tsx
b/modules/research-framework/portal/src/components/home/index.tsx
index 1007007e67..3a932951f7 100644
--- a/modules/research-framework/portal/src/components/home/index.tsx
+++ b/modules/research-framework/portal/src/components/home/index.tsx
@@ -53,6 +53,8 @@ const Home = () => {
</HStack>
</HStack>
<SessionsSection />
+
+ <Box my={8} />
</Container>
</Box>
);
diff --git a/modules/research-framework/portal/src/components/models/index.tsx
b/modules/research-framework/portal/src/components/models/index.tsx
index 0fa41c76b5..2ae3cd27b2 100644
--- a/modules/research-framework/portal/src/components/models/index.tsx
+++ b/modules/research-framework/portal/src/components/models/index.tsx
@@ -9,10 +9,19 @@ import api from "@/lib/api";
import { ModelResource } from "@/interfaces/ResourceType";
import { useEffect, useState } from "react";
import { ResourceCard } from "../home/ResourceCard";
+import { CONTROLLER } from "@/lib/controller";
+import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
const getModels = async () => {
try {
- const response = await api.get("/project-management/resources?type=MODEL");
+ const response = await api.get(`${CONTROLLER.resources}/`, {
+ params: {
+ type: ResourceTypeEnum.MODEL,
+ pageNumber: 0,
+ pageSize: 100,
+ },
+ });
+
const data = response.data;
return data;
} catch (error) {
diff --git
a/modules/research-framework/portal/src/components/notebooks/ProjectDetails.tsx
b/modules/research-framework/portal/src/components/notebooks/ProjectDetails.tsx
deleted file mode 100644
index 9b9141d712..0000000000
---
a/modules/research-framework/portal/src/components/notebooks/ProjectDetails.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-import { Link, useParams } from "react-router";
-import NavBar from "../NavBar";
-import {
- Container,
- Spinner,
- HStack,
- Box,
- Separator,
- Button,
- Icon,
- Link as ChakraLink, Heading, ListItem, Center, ListRoot,
-} from "@chakra-ui/react";
-// @ts-expect-error This is fine
-import { MOCK_PROJECTS } from "../../data/MOCK_DATA";
-import { useEffect, useState } from "react";
-import { BiArrowBack } from "react-icons/bi";
-import { ProjectType } from "@/interfaces/ProjectType";
-import { Metadata } from "../Metadata";
-import {FiFile, FiFolder} from "react-icons/fi";
-
-interface FileTreeItem {
- name: string;
- type: string;
- sha: string;
-}
-
-async function getProject(slug: string | undefined) {
- return MOCK_PROJECTS.find(
- (project: ProjectType) => project.metadata.slug === slug
- );
-}
-
-const ProjectDetails = () => {
- const { slug } = useParams();
- const [project, setProject] = useState<ProjectType | null>(null);
- const [fileTree, setFileTree] = useState<FileTreeItem[]>([]);
- const [fileTreeLoading, setFileTreeLoading] = useState(false);
-
- useEffect(() => {
- if (!slug) return;
-
- async function getData() {
- const n = await getProject(slug);
- setProject(n);
- }
- getData();
- }, [slug]);
-
- const isNotebook = project?.notebookViewer !== undefined;
-
- useEffect(() => {
- if (project && !isNotebook) {
- const repoUrl = project.repositoryUrl;
- // @ts-expect-error This is fine
- const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
- if (match) {
- const owner = match[1];
- const repo = match[2].replace(/\.git$/, "");
- setFileTreeLoading(true);
- fetch(`https://api.github.com/repos/${owner}/${repo}/contents`)
- .then((res) => res.json())
- .then((data) => {
- setFileTree(data);
- setFileTreeLoading(false);
- })
- .catch((error) => {
- console.error("Error fetching file tree:", error);
- setFileTreeLoading(false);
- });
- }
- }
- }, [project, isNotebook]);
-
- if (!project) return <Spinner />;
-
- return (
- <>
- <NavBar />
-
- <Container maxW="breakpoint-lg" mx="auto" p={4} mt={16}>
- <Box>
- <HStack alignItems="center" mb={4}>
- <Icon>
- <BiArrowBack />
- </Icon>
- <Link to="/">Back to Projects</Link>
- </HStack>
- </Box>
- <Metadata metadata={project.metadata} />
-
- <Separator my={8} />
-
- <Button colorPalette="teal" mb={4} w="full">
- <ChakraLink
- target="_blank"
- href={project?.notebookViewer || project?.repositoryUrl}
- color="white"
- fontWeight="bold"
- display="block"
- width="100%"
- textAlign="center"
- >
- Open
- </ChakraLink>
- </Button>
-
- <Box border="1px" borderColor="gray.200" borderRadius="md"
overflow="hidden">
- <Box height="600px" bg="gray.50" p={4} overflow="auto">
- {isNotebook ? (
- <iframe
- title="notebook"
- src={project?.notebookViewer}
- width="100%"
- height="100%"
- style={{ border: "none" }}
- />
- ) : fileTreeLoading ? (
- <Center height="100%">
- <Spinner size="xl" />
- </Center>
- ) : (
- <Box
- bg="white"
- p={4}
- borderRadius="md"
- shadow="md"
- overflow="auto"
- height="full"
- >
- <Heading as="h2" size="lg" mb={4}>
- {project.metadata.title}
- </Heading>
- <ListRoot>
- {fileTree &&
- fileTree.map((file) => (
- <ListItem
- key={file.sha}
- display="flex"
- alignItems="center"
- p={2}
- borderRadius="md"
- _hover={{ bg: "gray.100" }}
- >
- <Icon
- as={file.type === "dir" ? FiFolder :
FiFile}
- color={file.type === "dir" ? "blue.500" :
"gray.500"}
- mr={2}
- />
-
- <p>{file.name}</p>
- </ListItem>
- ))}
- </ListRoot>
- </Box>
- )}
- </Box>
- </Box>
- </Container>
- </>
- );
-};
-
-export default ProjectDetails;
diff --git
a/modules/research-framework/portal/src/components/notebooks/index.tsx
b/modules/research-framework/portal/src/components/notebooks/index.tsx
index 5730581c7f..7baa79aee7 100644
--- a/modules/research-framework/portal/src/components/notebooks/index.tsx
+++ b/modules/research-framework/portal/src/components/notebooks/index.tsx
@@ -7,12 +7,18 @@ import { useEffect, useState } from "react";
import api from "@/lib/api";
import { NotebookResource } from "@/interfaces/ResourceType";
import { ResourceCard } from "../home/ResourceCard";
+import { CONTROLLER } from "@/lib/controller";
+import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
const getNotebooks = async () => {
try {
- const response = await api.get(
- "/project-management/resources?type=NOTEBOOK"
- );
+ const response = await api.get(`${CONTROLLER.resources}/`, {
+ params: {
+ type: ResourceTypeEnum.NOTEBOOK,
+ pageNumber: 0,
+ pageSize: 100,
+ },
+ });
const data = response.data;
return data;
} catch (error) {
diff --git
a/modules/research-framework/portal/src/components/repositories/RepositoryCard.tsx
b/modules/research-framework/portal/src/components/repositories/RepositoryCard.tsx
deleted file mode 100644
index 5d5f2ba8e4..0000000000
---
a/modules/research-framework/portal/src/components/repositories/RepositoryCard.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import {
- Card,
- Heading,
- Box,
- Text,
- Link as ChakraLink,
- HStack,
-} from "@chakra-ui/react";
-export const RepositoryCard = ({ repository }) => {
- return (
- <Card.Root width="100%">
- <Card.Header>
- <HStack justifyContent="space-between" alignItems="flex-start">
- <Box>
- <Heading size="md">{repository.title}</Heading>
- <Text color="gray.500" mt={2}>
- {repository.date}
- </Text>
- </Box>
-
- <Text>
- <ChakraLink
- href={repository.githubUrl}
- target="_blank"
- bg="black"
- color="white"
- p={2}
- rounded="md"
- fontWeight="bold"
- >
- View on GitHub
- </ChakraLink>
- </Text>
- </HStack>
- </Card.Header>
- <Card.Body color="fg.muted">{repository.description}</Card.Body>
- </Card.Root>
- );
-};
diff --git
a/modules/research-framework/portal/src/components/repositories/RepositorySpecificDetails.tsx
b/modules/research-framework/portal/src/components/repositories/RepositorySpecificDetails.tsx
index 2dd16cb69e..ba1f3f08a3 100644
---
a/modules/research-framework/portal/src/components/repositories/RepositorySpecificDetails.tsx
+++
b/modules/research-framework/portal/src/components/repositories/RepositorySpecificDetails.tsx
@@ -32,8 +32,6 @@ export const RepositorySpecificDetails = ({
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(() => {
@@ -105,7 +103,6 @@ export const RepositorySpecificDetails = ({
const owner = ownerAndRepo.owner;
const repo = ownerAndRepo.repo;
fetchFileContent(owner, repo, path);
- setFileName(path);
}
console.log("File clicked:", path);
@@ -117,7 +114,6 @@ export const RepositorySpecificDetails = ({
setHistory((prev) => prev.slice(0, -1));
setCurrentPath(previousPath);
setFileContent(null);
- setFileName(null);
}
};
diff --git
a/modules/research-framework/portal/src/components/repositories/index.tsx
b/modules/research-framework/portal/src/components/repositories/index.tsx
index 1100620a9b..665c6ab724 100644
--- a/modules/research-framework/portal/src/components/repositories/index.tsx
+++ b/modules/research-framework/portal/src/components/repositories/index.tsx
@@ -7,12 +7,18 @@ import { RepositoryResource } from
"@/interfaces/ResourceType";
import { ResourceCard } from "../home/ResourceCard";
import { LuSearch } from "react-icons/lu";
import { InputGroup } from "../ui/input-group";
+import { CONTROLLER } from "@/lib/controller";
+import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
const getRepositories = async () => {
try {
- const response = await api.get(
- "/project-management/resources?type=REPOSITORY"
- );
+ const response = await api.get(`${CONTROLLER.resources}/`, {
+ params: {
+ type: ResourceTypeEnum.REPOSITORY,
+ pageNumber: 0,
+ pageSize: 100,
+ },
+ });
const data = response.data;
return data;
} catch (error) {
diff --git
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
index 0841b6dd3b..a475cf302d 100644
---
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
+++
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
@@ -1,4 +1,4 @@
-import { Link, useParams } from "react-router";
+import { useNavigate, useParams } from "react-router";
import NavBar from "../NavBar";
import {
Container,
@@ -12,6 +12,7 @@ import {
Badge,
Heading,
Avatar,
+ Button,
} from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { BiArrowBack } from "react-icons/bi";
@@ -23,22 +24,23 @@ import {
Resource,
} from "@/interfaces/ResourceType";
import { Tag } from "@/interfaces/TagType";
-import { User } from "@/interfaces/UserType";
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";
async function getResource(id: string) {
- const response = await api.get(`/project-management/resources/${id}`);
+ const response = await api.get(`${CONTROLLER.resources}/${id}`);
return response.data;
}
const ResourceDetails = () => {
const { id } = useParams();
const [resource, setResource] = useState<Resource | null>(null);
+ const navigate = useNavigate();
useEffect(() => {
if (!id) return;
@@ -61,7 +63,7 @@ const ResourceDetails = () => {
<NavBar />
<Container maxW="breakpoint-lg" mx="auto" p={4} mt={16}>
<Box>
- <Link to="/">
+ <Button variant="plain" p={0} onClick={() => navigate(-1)}>
<HStack
alignItems="center"
mb={4}
@@ -77,7 +79,7 @@ const ResourceDetails = () => {
</Icon>
Back
</HStack>
- </Link>
+ </Button>
</Box>
<HStack
@@ -106,20 +108,16 @@ const ResourceDetails = () => {
</HStack>
<HStack mt={8}>
- {resource.authors.map((author: User) => {
+ {resource.authors.map((author: string) => {
return (
- <HStack key={author.id}>
+ <HStack key={author}>
<Avatar.Root shape="full" size="xl">
- <Avatar.Fallback
- name={author.firstName + " " + author.lastName}
- />
- <Avatar.Image src={author.avatar} />
+ <Avatar.Fallback name={author} />
+ <Avatar.Image src={author} />
</Avatar.Root>
<Box>
- <Text fontWeight="bold">
- {author.firstName + " " + author.lastName}
- </Text>
+ <Text fontWeight="bold">{author}</Text>
</Box>
</HStack>
);
diff --git a/modules/research-framework/portal/src/components/ui/toaster.tsx
b/modules/research-framework/portal/src/components/ui/toaster.tsx
new file mode 100644
index 0000000000..df6c2c38ee
--- /dev/null
+++ b/modules/research-framework/portal/src/components/ui/toaster.tsx
@@ -0,0 +1,43 @@
+"use client"
+
+import {
+ Toaster as ChakraToaster,
+ Portal,
+ Spinner,
+ Stack,
+ Toast,
+ createToaster,
+} from "@chakra-ui/react"
+
+export const toaster = createToaster({
+ placement: "bottom-end",
+ pauseOnPageIdle: true,
+})
+
+export const Toaster = () => {
+ return (
+ <Portal>
+ <ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
+ {(toast) => (
+ <Toast.Root width={{ md: "sm" }}>
+ {toast.type === "loading" ? (
+ <Spinner size="sm" color="blue.solid" />
+ ) : (
+ <Toast.Indicator />
+ )}
+ <Stack gap="1" flex="1" maxWidth="100%">
+ {toast.title && <Toast.Title>{toast.title}</Toast.Title>}
+ {toast.description && (
+ <Toast.Description>{toast.description}</Toast.Description>
+ )}
+ </Stack>
+ {toast.action && (
+ <Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
+ )}
+ {toast.meta?.closable && <Toast.CloseTrigger />}
+ </Toast.Root>
+ )}
+ </ChakraToaster>
+ </Portal>
+ )
+}
diff --git a/modules/research-framework/portal/src/interfaces/ResourceType.ts
b/modules/research-framework/portal/src/interfaces/ResourceType.ts
index 273e435ba4..32d4f697a8 100644
--- a/modules/research-framework/portal/src/interfaces/ResourceType.ts
+++ b/modules/research-framework/portal/src/interfaces/ResourceType.ts
@@ -2,14 +2,14 @@ import { PrivacyEnum } from "./PrivacyEnum";
import { ResourceTypeEnum } from "./ResourceTypeEnum";
import { StatusEnum } from "./StatusEnum";
import { Tag } from "./TagType";
-import { User } from "./UserType";
+// import { User } from "./UserType";
export interface Resource {
id: string; // UUID, immutable
name: string;
description: string;
headerImage: string;
- authors: User[];
+ authors: string[];
tags: Tag[];
status: StatusEnum;
privacy: PrivacyEnum;
diff --git a/modules/research-framework/portal/src/lib/api.ts
b/modules/research-framework/portal/src/lib/api.ts
index 62ee3f6ab0..054439ad85 100644
--- a/modules/research-framework/portal/src/lib/api.ts
+++ b/modules/research-framework/portal/src/lib/api.ts
@@ -1,5 +1,6 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { API_VERSION, BACKEND_URL } from './constants';
+import { User } from 'oidc-client-ts';
const api: AxiosInstance = axios.create({
baseURL: `${BACKEND_URL}/api/${API_VERSION}/rf`,
@@ -10,9 +11,9 @@ const api: AxiosInstance = axios.create({
});
// Function to inject token dynamically
-let getUser: (() => Promise<any | null>) | null = null;
+let getUser: (() => Promise<User | null>) | null = null;
-export const setUserProvider = (provider: () => Promise<string | null>) => {
+export const setUserProvider = (provider: () => Promise<User | null>) => {
getUser = provider;
};
@@ -30,9 +31,6 @@ api.interceptors.request.use(
}
}
- console.log("configuration", config);
-
-
return config;
},
(error) => Promise.reject(error)
diff --git a/modules/research-framework/portal/src/lib/constants.ts
b/modules/research-framework/portal/src/lib/constants.ts
index 27d336f3e3..34bbea7b8f 100644
--- a/modules/research-framework/portal/src/lib/constants.ts
+++ b/modules/research-framework/portal/src/lib/constants.ts
@@ -1,15 +1,17 @@
let BACKEND_URL: string;
-const API_VERSION = "v1";
+let APP_URL: string;
if (process.env.NODE_ENV === 'production') {
- BACKEND_URL = "";
+ BACKEND_URL = "https://api.cybershuttle.org:18889";
+ APP_URL = "https://catalog.dev.cybershuttle.org";
} else {
BACKEND_URL = "http://localhost:18889";
+ APP_URL = 'http://localhost:5173';
}
+export const API_VERSION = "v1";
export const CLIENT_ID = 'data-catalog-portal';
-export const APP_URL = 'http://localhost:5173';
export const APP_REDIRECT_URI = `${APP_URL}/oauth_callback`;
export const OPENID_CONFIG_URL =
`https://auth.dev.cybershuttle.org/realms/default/.well-known/openid-configuration`;
-export { BACKEND_URL, API_VERSION };
\ No newline at end of file
+export { BACKEND_URL, APP_URL };
diff --git a/modules/research-framework/portal/src/lib/controller.ts
b/modules/research-framework/portal/src/lib/controller.ts
new file mode 100644
index 0000000000..866e82e630
--- /dev/null
+++ b/modules/research-framework/portal/src/lib/controller.ts
@@ -0,0 +1,6 @@
+export const CONTROLLER = {
+ projects: "/projects",
+ hub: "/hub",
+ resources: "/resources",
+ sessions: "/sessions",
+}
\ No newline at end of file
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/ResponseTypes/RedirectResponse.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/ResponseTypes/RedirectResponse.java
new file mode 100644
index 0000000000..abc4bfaf9b
--- /dev/null
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/ResponseTypes/RedirectResponse.java
@@ -0,0 +1,17 @@
+package org.apache.airavata.research.service.ResponseTypes;
+
+public class RedirectResponse {
+ private String redirectUrl;
+
+ public RedirectResponse(String redirectUrl) {
+ this.redirectUrl = redirectUrl;
+ }
+
+ public String getRedirectUrl() {
+ return redirectUrl;
+ }
+
+ public void setRedirectUrl(String redirectUrl) {
+ this.redirectUrl = redirectUrl;
+ }
+}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ProjectController.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ProjectController.java
new file mode 100644
index 0000000000..d3f5c780ec
--- /dev/null
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ProjectController.java
@@ -0,0 +1,28 @@
+package org.apache.airavata.research.service.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.airavata.research.service.handlers.ProjectHandler;
+import org.apache.airavata.research.service.model.entity.Project;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/v1/rf/projects")
+@Tag(name = "Projects", description = "Projects are comprised of dataset and
repository resources")
+public class ProjectController {
+
+ @Autowired
+ private ProjectHandler projectHandler;
+
+ @GetMapping("/")
+ @Operation(summary = "Get all projects")
+ public ResponseEntity<List<Project>> getAllProjects() {
+ return ResponseEntity.ok(projectHandler.getAllProjects());
+ }
+}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResearchHubController.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResearchHubController.java
index 9a5f46881c..9a2e6bed28 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResearchHubController.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/ResearchHubController.java
@@ -20,6 +20,7 @@ package org.apache.airavata.research.service.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.airavata.research.service.ResponseTypes.RedirectResponse;
import org.apache.airavata.research.service.enums.SessionStatusEnum;
import org.apache.airavata.research.service.handlers.ResearchHubHandler;
import org.apache.airavata.research.service.handlers.SessionHandler;
@@ -46,55 +47,33 @@ public class ResearchHubController {
private final ResearchHubHandler rHubHandler;
- @Autowired
- private SessionHandler sessionHandler;
-
public ResearchHubController(ResearchHubHandler rHubHandler) {
this.rHubHandler = rHubHandler;
}
- @GetMapping("/projects")
- @Operation(summary = "Get all projects")
- public ResponseEntity<List<Project>> getAllProjects() {
- return ResponseEntity.ok(rHubHandler.getAllProjects());
- }
-
- @GetMapping("/project/{projectId}")
+ @GetMapping("/start/project/{projectId}")
@Operation(summary = "Spawn new project session")
- public ResponseEntity<?> resolveRHubUrl(@PathVariable("projectId") String
projectId, @RequestParam("sessionName") String sessionName) {
- String spawnUrl = rHubHandler.spinRHubSession(projectId, sessionName);
-
- LOGGER.info("Redirecting user to spawn URL: {}", spawnUrl);
- return
ResponseEntity.status(HttpStatus.FOUND).location(URI.create(spawnUrl)).build();
- }
+ public ResponseEntity<RedirectResponse>
resolveRHubUrl(@PathVariable("projectId") String projectId,
@RequestParam("sessionName") String sessionName) {
+ LOGGER.info("Starting new RHub session ({}) for project: {}",
sessionName, projectId);
- @GetMapping("/project/{projectId}/exists")
- @Operation(summary = "Check if session with project already exists")
- public ResponseEntity<Boolean>
getProjectSessionExists(@PathVariable("projectId") String projectId) {
+ String spawnUrl = rHubHandler.spinRHubSession(projectId, sessionName);
- return
ResponseEntity.ok(sessionHandler.checkIfSessionExists(projectId,
UserContext.userId()));
- }
+ LOGGER.info("Session spawned: {}", spawnUrl);
- @GetMapping("/sessions")
- @Operation(summary = "Get all sessions for a given user")
- public ResponseEntity<List<Session>> getSessions() {
- String userId = UserContext.userId();
- List<Session> sessions = sessionHandler.findAllByUserId(userId);
- return ResponseEntity.ok(sessions);
- }
+ RedirectResponse response = new RedirectResponse(spawnUrl);
- @PatchMapping("/sessions/{sessionId}")
- @Operation(summary = "Update a session's status")
- public ResponseEntity<Session>
updateSessionStatus(@PathVariable(value="sessionId") String sessionId,
@Param(value="status") SessionStatusEnum status) {
- return ResponseEntity.ok(sessionHandler.updateSessionStatus(sessionId,
status));
+ return ResponseEntity.ok(response);
}
- @GetMapping("/sessions/{sessionId}/resolve")
- public ResponseEntity<?>
resolveRHubExistingSession(@PathVariable("sessionId") String sessionId) {
+ @GetMapping("/resume/session/{sessionId}")
+ public ResponseEntity<RedirectResponse>
resolveRHubExistingSession(@PathVariable("sessionId") String sessionId) {
+ LOGGER.info("Resuming session: {}", sessionId);
String spawnUrl = rHubHandler.resolveRHubExistingSession(sessionId);
+ LOGGER.info("Resume success: {}", spawnUrl);
+
+ RedirectResponse response = new RedirectResponse(spawnUrl);
- LOGGER.info("Redirecting to existing session spawn URL: {}", spawnUrl);
- return
ResponseEntity.status(HttpStatus.FOUND).location(URI.create(spawnUrl)).build();
+ return ResponseEntity.ok(response);
}
}
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 e3fd758782..d61c4e198e 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
@@ -39,12 +39,11 @@ import java.util.ArrayList;
import java.util.List;
@RestController
-@RequestMapping("/api/v1/rf/project-management")
-@Tag(name = "Project", description = "The Project API")
+@RequestMapping("/api/v1/rf/resources")
+@Tag(name = "Resources", description = "Datasets, notebooks, repositories,
models")
public class ResourceController {
private static final Logger LOGGER =
LoggerFactory.getLogger(ResourceController.class);
-
@org.springframework.beans.factory.annotation.Autowired
private ResourceHandler resourceHandler;
@@ -75,7 +74,7 @@ public class ResourceController {
@Operation(
summary = "Get dataset, notebook, or repository"
)
- @GetMapping(value = "/resources/{id}")
+ @GetMapping(value = "/{id}")
public ResponseEntity<Resource> getResource(@PathVariable(value="id")
String id) {
return ResponseEntity.ok(resourceHandler.getResourceById(id));
}
@@ -83,7 +82,7 @@ public class ResourceController {
@Operation(
summary = "Get all resources"
)
- @GetMapping("/resources")
+ @GetMapping("/")
public ResponseEntity<Page<Resource>> getAllResources(
@RequestParam(value="pageNumber", defaultValue = "0") int
pageNumber,
@RequestParam(value="pageSize", defaultValue = "10") int pageSize,
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/SessionController.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/SessionController.java
new file mode 100644
index 0000000000..a0261f9276
--- /dev/null
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/SessionController.java
@@ -0,0 +1,50 @@
+package org.apache.airavata.research.service.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.airavata.research.service.enums.SessionStatusEnum;
+import org.apache.airavata.research.service.handlers.ResearchHubHandler;
+import org.apache.airavata.research.service.handlers.SessionHandler;
+import org.apache.airavata.research.service.model.UserContext;
+import org.apache.airavata.research.service.model.entity.Session;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.repository.query.Param;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.net.URI;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/v1/rf/sessions")
+@Tag(name = "Sessions", description = "All operations related to sessions
(created from projects")
+public class SessionController {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SessionController.class);
+
+ @Autowired
+ private SessionHandler sessionHandler;
+
+ @GetMapping("/")
+ @Operation(summary = "Get all sessions by session status and userId")
+ public ResponseEntity<List<Session>>
getSessions(@RequestParam(value="status", required = false) SessionStatusEnum
status) {
+ LOGGER.info("Getting all sessions for user: {}, status filter: {}",
UserContext.userId(), status);
+ String userId = UserContext.userId();
+ List<Session> sessions;
+ if (status == null) {
+ sessions = sessionHandler.findAllByUserId(userId);
+ } else {
+ sessions = sessionHandler.findAllByUserIdAndStatus(userId, status);
+ }
+ return ResponseEntity.ok(sessions);
+ }
+
+ @PatchMapping("/{sessionId}")
+ @Operation(summary = "Update a session's status")
+ public ResponseEntity<Session>
updateSessionStatus(@PathVariable(value="sessionId") String sessionId,
@RequestParam(value="status") SessionStatusEnum status) {
+ LOGGER.info("Updating session status for session: {} to {}",
sessionId, status);
+ return ResponseEntity.ok(sessionHandler.updateSessionStatus(sessionId,
status));
+ }
+}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ProjectHandler.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ProjectHandler.java
index eb76fcf939..8b1425015c 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ProjectHandler.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ProjectHandler.java
@@ -25,6 +25,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
+import java.util.List;
+
@Service
public class ProjectHandler {
@@ -42,4 +44,8 @@ public class ProjectHandler {
return new EntityNotFoundException("Unable to find a Project with
id: " + projectId);
});
}
+
+ public List<Project> getAllProjects() {
+ return projectRepository.findAll();
+ }
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
index 3f5f4cf923..d5fbc60220 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResearchHubHandler.java
@@ -70,8 +70,4 @@ public class ResearchHubHandler {
LOGGER.debug("Generated the session url: {} for the user: {}",
sessionUrl, UserContext.userId());
return sessionUrl;
}
-
- public List<Project> getAllProjects() {
- return projectRepository.findAll();
- }
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
index e3c25c75e2..486c7d69db 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/SessionHandler.java
@@ -60,8 +60,11 @@ public class SessionHandler {
}
public List<Session> findAllByUserId(String userId) {
- List<Session> sessions = sessionRepository.findByUserId(userId);
- return sessions;
+ return sessionRepository.findByUserIdOrderByCreatedAtDesc(userId);
+ }
+
+ public List<Session> findAllByUserIdAndStatus(String userId,
SessionStatusEnum status) {
+ return
sessionRepository.findByUserIdAndStatusOrderByCreatedAtDesc(userId, status);
}
public Session updateSessionStatus(String sessionId, SessionStatusEnum
status) {
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
index 42e9ad95e5..15fe1ea08b 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/SessionRepository.java
@@ -18,6 +18,7 @@
*/
package org.apache.airavata.research.service.model.repo;
+import org.apache.airavata.research.service.enums.SessionStatusEnum;
import org.apache.airavata.research.service.model.entity.Session;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
@@ -31,4 +32,10 @@ public interface SessionRepository extends
JpaRepository<Session, String> {
@Query(value = "SELECT * FROM session WHERE project_id = :projectId AND
user_id = :userId", nativeQuery = true)
Optional<Session> findSessionByProjectIdAndUserId(@Param("projectId")
String projectId, @Param("userId") String userId);
+
+ List<Session> findByUserIdAndStatus(String userId, SessionStatusEnum
status);
+
+ List<Session> findByUserIdOrderByCreatedAtDesc(String userId);
+
+ List<Session> findByUserIdAndStatusOrderByCreatedAtDesc(String userId,
SessionStatusEnum status);
}
diff --git
a/modules/research-framework/research-service/src/main/resources/application.yml
b/modules/research-framework/research-service/src/main/resources/application.yml
index 2c80d31bfd..42b99ebf12 100644
---
a/modules/research-framework/research-service/src/main/resources/application.yml
+++
b/modules/research-framework/research-service/src/main/resources/application.yml
@@ -9,9 +9,10 @@ server:
airavata:
research-hub:
url: https://hub.dev.cybershuttle.org
- dev-user: "[email protected]"
+ dev-user: "[email protected]"
research-portal:
url: http://localhost:5173
+
user-profile:
server:
url: api.dev.cybershuttle.org