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

dimuthuupe 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 df26dd3e92 Support session termination & deletion on jupyter
df26dd3e92 is described below

commit df26dd3e922c6169d81d4950e26287059485d18e
Author: ganning127 <[email protected]>
AuthorDate: Thu Apr 10 22:58:40 2025 -0400

    Support session termination & deletion on jupyter
---
 modules/research-framework/portal/src/App.tsx      |  2 +-
 .../auth/{Login.tsx => UserLoginPage.tsx}          |  0
 .../portal/src/components/home/SessionCard.tsx     | 18 +++---
 .../home/StartSessionFromProjectButton.tsx         | 13 +++-
 .../service/config/DevDataInitializer.java         |  2 +-
 .../controller/GlobalExceptionController.java      | 12 +++-
 .../service/handlers/ResearchHubHandler.java       | 75 +++++++++++++++++++++-
 .../research/service/handlers/SessionHandler.java  | 33 +++++++---
 .../service/model/repo/SessionRepository.java      |  2 +
 .../src/main/resources/application.yml             | 11 ++--
 10 files changed, 137 insertions(+), 31 deletions(-)

diff --git a/modules/research-framework/portal/src/App.tsx 
b/modules/research-framework/portal/src/App.tsx
index 028a5ed28a..5433a82ec0 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/UserLoginPage";
 import ProtectedComponent from "./components/auth/ProtectedComponent";
 import { AuthProvider, AuthProviderProps } from "react-oidc-context";
 import { useEffect, useState } from "react";
diff --git a/modules/research-framework/portal/src/components/auth/Login.tsx 
b/modules/research-framework/portal/src/components/auth/UserLoginPage.tsx
similarity index 100%
rename from modules/research-framework/portal/src/components/auth/Login.tsx
rename to 
modules/research-framework/portal/src/components/auth/UserLoginPage.tsx
diff --git 
a/modules/research-framework/portal/src/components/home/SessionCard.tsx 
b/modules/research-framework/portal/src/components/home/SessionCard.tsx
index 96123a000d..2bd14a2d95 100644
--- a/modules/research-framework/portal/src/components/home/SessionCard.tsx
+++ b/modules/research-framework/portal/src/components/home/SessionCard.tsx
@@ -116,14 +116,16 @@ export const SessionCard = ({ session }: { session: 
SessionType }) => {
               <Heading size="lg">{session.sessionName}</Heading>
             </Box>
             <VStack alignItems="flex-end">
-              <IconButton
-                color="red.600"
-                size="xs"
-                variant={"ghost"}
-                onClick={() => dialog.setOpen(true)}
-              >
-                <FaTrash />
-              </IconButton>
+              {session.status === SessionStatusEnum.TERMINATED && (
+                <IconButton
+                  color="red.600"
+                  size="xs"
+                  variant={"ghost"}
+                  onClick={() => dialog.setOpen(true)}
+                >
+                  <FaTrash />
+                </IconButton>
+              )}
             </VStack>
           </HStack>
         </Card.Header>
diff --git 
a/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
 
b/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
index bbd734525d..5d5d2be100 100644
--- 
a/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
+++ 
b/modules/research-framework/portal/src/components/home/StartSessionFromProjectButton.tsx
@@ -14,6 +14,7 @@ import { useState } from "react";
 import { Toaster, toaster } from "../ui/toaster";
 import { useAuth } from "react-oidc-context";
 import { useNavigate } from "react-router";
+import { AxiosError } from "axios";
 
 export const StartSessionFromProjectButton = ({
   project,
@@ -67,11 +68,17 @@ export const StartSessionFromProjectButton = ({
         type: "success",
       });
     } catch (error) {
-      console.error("Error fetching project:", error);
+      const err = error as AxiosError<unknown>;
+      let msg: string = (err.response?.data as { message: string })?.message;
+
+      if (!msg) {
+        msg =
+          "This is likely because you just made an account and haven't been 
enabled yet. Please let us know so we can enable your account";
+      }
+
       toaster.create({
         title: "Error starting session",
-        description:
-          "This is likely because you just made an account and haven't been 
enabled yet. Please let us know so we can enable your account.",
+        description: msg,
         type: "error",
       });
     }
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
index 2834e04d40..ae7b31c809 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/DevDataInitializer.java
@@ -105,7 +105,7 @@ public class DevDataInitializer implements 
CommandLineRunner {
 
     @Override
     public void run(String... args) {
-        if (projectRepository.existsByOwnerId(devUserEmail)) {
+        if (projectRepository.count() > 0) {
             System.out.println("Dev data already initialized. Skipping 
initialization.");
             return;
         }
diff --git 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
index af053d40a4..28c2ddcf6d 100644
--- 
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
+++ 
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/controller/GlobalExceptionController.java
@@ -26,6 +26,9 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.ControllerAdvice;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @ControllerAdvice
 public class GlobalExceptionController {
 
@@ -48,10 +51,13 @@ public class GlobalExceptionController {
     }
 
     @ExceptionHandler(Exception.class)
-    public ResponseEntity<String> handleOtherExceptions(Exception ex) {
+    public ResponseEntity<Map<String, String>> handleOtherExceptions(Exception 
ex) {
         LOGGER.error("Unexpected error occurred: ", ex);
+        Map<String, String> errorResponse = new HashMap<>();
+        errorResponse.put("message", ex.getMessage());
+
         return ResponseEntity
                 .status(HttpStatus.INTERNAL_SERVER_ERROR)
-                .body("Unexpected error occurred");
+                .body(errorResponse);
     }
-}
\ No newline at end of file
+}
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 d5fbc60220..de5484b481 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
@@ -18,6 +18,7 @@
  */
 package org.apache.airavata.research.service.handlers;
 
+import org.apache.airavata.research.service.enums.SessionStatusEnum;
 import org.apache.airavata.research.service.model.UserContext;
 import org.apache.airavata.research.service.model.entity.DatasetResource;
 import org.apache.airavata.research.service.model.entity.Project;
@@ -27,8 +28,14 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
 
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 
 @Service
 public class ResearchHubHandler {
@@ -37,6 +44,8 @@ public class ResearchHubHandler {
     private static final String RH_SPAWN_URL = 
"%s/hub/spawn/%s/%s?git=%s&dataPath=%s";
     private static final String RH_SESSION_URL = "%s/hub/spawn/%s/%s";
 
+    private static final String SERVERS_API_URL = 
"%s/hub/api/users/%s/servers/%s";
+
     private final ProjectHandler projectHandler;
     private final SessionHandler sessionHandler;
     private final ProjectRepository projectRepository;
@@ -44,13 +53,77 @@ public class ResearchHubHandler {
     @Value("${airavata.research-hub.url}")
     private String csHubUrl;
 
+    @Value("${airavata.research-hub.adminApiKey}")
+    private String adminApiKey;
+
+    @Value("${airavata.research-hub.limit}")
+    private int maxRHubSessions;
+
     public ResearchHubHandler(ProjectHandler projectHandler, SessionHandler 
sessionHandler, ProjectRepository projectRepository) {
         this.projectHandler = projectHandler;
         this.sessionHandler = sessionHandler;
         this.projectRepository = projectRepository;
     }
 
+    public boolean stopSession(String sessionId) {
+        String userId = UserContext.userId();
+        String url = String.format(SERVERS_API_URL, csHubUrl, userId, 
sessionId);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Authorization", "token " + adminApiKey);
+        HttpEntity<Void> request = new HttpEntity<>(headers);
+
+        ResponseEntity<Void> response = new RestTemplate().exchange(
+                url,
+                HttpMethod.DELETE,
+                request,
+                Void.class
+        );
+
+        if (response.getStatusCode().is2xxSuccessful()) {
+            LOGGER.info("Successfully stopped/deleted RHub session {} for user 
{}", sessionId, userId);
+            return true;
+        } else {
+            throw new RuntimeException("Failed to delete RHub session " + 
sessionId + " for user " + userId);
+        }
+    }
+
+    public boolean deleteSession(String sessionId) {
+        String userId = UserContext.userId();
+        String url = String.format(SERVERS_API_URL, csHubUrl, userId, 
sessionId);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("Authorization", "token " + adminApiKey);
+
+        Map<String, Object> body = new HashMap<>();
+        body.put("remove", true);
+
+        HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, 
headers);
+
+        ResponseEntity<Void> response = new RestTemplate().exchange(
+                url,
+                HttpMethod.DELETE,
+                request,
+                Void.class
+        );
+
+        if (response.getStatusCode().is2xxSuccessful()) {
+            LOGGER.info("Successfully stopped/deleted RHub session {} for user 
{}", sessionId, userId);
+            return true;
+        } else {
+            throw new RuntimeException("Failed to delete RHub session " + 
sessionId + " for user " + userId);
+        }
+    }
+
+
+
     public String spinRHubSession(String projectId, String sessionName) {
+        String userId = UserContext.userId();
+        int alreadyCreated = 
sessionHandler.countSessionsByUserIdAndStatus(userId, 
SessionStatusEnum.CREATED);
+        if (alreadyCreated >= maxRHubSessions) {
+            throw new RuntimeException("Max number of active sessions (10) has 
already been reached. Please terminate or delete a session to continue.");
+        }
+
         Project project = projectHandler.findProject(projectId);
         // TODO should support multiple data sets for RHub
         DatasetResource dataset = 
project.getDatasetResources().stream().findFirst().get();
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 6f9e666ceb..75bf7ae3fe 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
@@ -27,6 +27,7 @@ import 
org.apache.airavata.research.service.model.repo.SessionRepository;
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 
 import java.util.List;
@@ -39,8 +40,11 @@ public class SessionHandler {
 
     private final SessionRepository sessionRepository;
 
-    public SessionHandler(SessionRepository sessionRepository) {
+    private final ResearchHubHandler researchHubHandler;
+
+    public SessionHandler(SessionRepository sessionRepository, @Lazy 
ResearchHubHandler researchHubHandler) {
         this.sessionRepository = sessionRepository;
+        this.researchHubHandler = researchHubHandler;
     }
 
     public Session findSession(String sessionId) {
@@ -69,23 +73,34 @@ public class SessionHandler {
 
     public Session updateSessionStatus(String sessionId, SessionStatusEnum 
status) {
         Session session = findSession(sessionId);
+
+        String userId = UserContext.userId();
+        if (!session.getUserId().equals(userId)) {
+            throw new RuntimeException("User is not authorized to update 
session");
+        }
+
+        if (status == SessionStatusEnum.TERMINATED) {
+            researchHubHandler.stopSession(sessionId);
+        }
+
         session.setStatus(status);
         session = sessionRepository.save(session);
         LOGGER.debug("Updated session with Id: {}, Status: {}", 
session.getId(), status);
         return session;
     }
 
-    public boolean deleteSession(String sessionId) {
-        Session session = findSession(sessionId);
-        sessionRepository.delete(session);
-        return true;
+    public int countSessionsByUserIdAndStatus(String userId, SessionStatusEnum 
status) {
+        return sessionRepository.countSessionsByUserIdAndStatus(userId, 
status);
     }
 
-    public boolean checkIfSessionExists(String projectId, String userId) {
-        if (sessionRepository.findSessionByProjectIdAndUserId(projectId, 
userId).isEmpty()) {
-            throw new RuntimeException("Session does not exist");
+    public boolean deleteSession(String sessionId) {
+        Session session = findSession(sessionId);
+        if (!session.getUserId().equals(UserContext.userId())) {
+            throw new RuntimeException("Invalid session ID");
         }
+
+        researchHubHandler.deleteSession(sessionId);
+        sessionRepository.delete(session);
         return true;
     }
-
 }
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 15fe1ea08b..2c2dc04a58 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
@@ -38,4 +38,6 @@ public interface SessionRepository extends 
JpaRepository<Session, String> {
     List<Session> findByUserIdOrderByCreatedAtDesc(String userId);
 
     List<Session> findByUserIdAndStatusOrderByCreatedAtDesc(String userId, 
SessionStatusEnum status);
+
+    int countSessionsByUserIdAndStatus(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 ac21eb7ba2..2144268d25 100644
--- 
a/modules/research-framework/research-service/src/main/resources/application.yml
+++ 
b/modules/research-framework/research-service/src/main/resources/application.yml
@@ -8,17 +8,20 @@ server:
 
 airavata:
   research-hub:
-    url: https://hub.dev.cybershuttle.org
+    url: https://hub.cybershuttle.org
     dev-user: "[email protected]"
+    adminApiKey: "JUPYTER_ADMIN_API_KEY"
+    limit: 10
+
   research-portal:
     url: http://localhost:5173
 
   openid:
-    url: 
"https://auth.dev.cybershuttle.org/realms/default/.well-known/openid-configuration";
+    url: 
"https://auth.cybershuttle.org/realms/default/.well-known/openid-configuration";
 
   user-profile:
     server:
-      url: api.dev.cybershuttle.org
+      url: api.cybershuttle.org
       port: 8962
 
 spring:
@@ -51,5 +54,3 @@ springdoc:
       use-pkce-with-authorization-code-grant: true
       client-id: data-catalog-portal
 
-
-

Reply via email to