This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata.git
The following commit(s) were added to refs/heads/master by this push:
new 58b8abc791 Support resource deletion (#522)
58b8abc791 is described below
commit 58b8abc79192997156ccee00f0695a3ad76f9bdf
Author: Ganning Xu <[email protected]>
AuthorDate: Wed Jun 18 07:14:56 2025 -0700
Support resource deletion (#522)
* support resource deletion
* refactoring & error logging
* support project deletion
---
.../portal/src/components/add/AddGitUrl.tsx | 70 +++---
.../portal/src/components/add/AddProjectMaster.tsx | 133 +++++-----
.../portal/src/components/add/DatasetSearch.tsx | 196 +++++++--------
.../portal/src/components/add/RepoSearch.tsx | 179 +++++++------
.../portal/src/components/home/ProjectCard.tsx | 64 +++--
.../portal/src/components/home/ResourceCard.tsx | 128 +++++-----
.../portal/src/components/notebooks/index.tsx | 71 +++---
.../components/projects/DeleteProjectButton.tsx | 115 +++++++++
.../components/resources/DeleteResourceButton.tsx | 115 +++++++++
.../src/components/resources/ResourceDetails.tsx | 276 +++++++++++----------
.../src/components/resources/ResourceTypeBadge.tsx | 33 +--
.../portal/src/components/resources/index.tsx | 1 -
.../portal/src/interfaces/ProjectType.tsx | 3 +-
modules/research-framework/portal/src/lib/util.ts | 13 +-
.../research/service/config/AuthzTokenFilter.java | 4 +-
.../service/controller/ProjectController.java | 14 +-
.../service/controller/ResourceController.java | 9 +-
.../airavata/research/service/enums/StateEnum.java | 25 ++
.../research/service/handlers/ProjectHandler.java | 31 ++-
.../research/service/handlers/ResourceHandler.java | 43 +++-
.../research/service/model/entity/Project.java | 15 ++
.../research/service/model/entity/Resource.java | 21 +-
.../service/model/repo/ProjectRepository.java | 15 ++
.../service/model/repo/ResourceRepository.java | 9 +-
.../src/main/resources/application.yml | 2 +-
25 files changed, 992 insertions(+), 593 deletions(-)
diff --git a/modules/research-framework/portal/src/components/add/AddGitUrl.tsx
b/modules/research-framework/portal/src/components/add/AddGitUrl.tsx
index 4ae21da8f4..79f2a2183a 100644
--- a/modules/research-framework/portal/src/components/add/AddGitUrl.tsx
+++ b/modules/research-framework/portal/src/components/add/AddGitUrl.tsx
@@ -1,15 +1,15 @@
-import { Button, Code, Input, Text, VStack } from "@chakra-ui/react";
-import { useState } from "react";
+import {Button, Code, Input, Text, VStack} from "@chakra-ui/react";
+import {useState} from "react";
import yaml from "js-yaml";
-import { CreateResourceRequest } from
"@/interfaces/Requests/CreateResourceRequest";
+import {CreateResourceRequest} from
"@/interfaces/Requests/CreateResourceRequest";
export const AddGitUrl = ({
- nextStage,
- createResourceRequest,
- setCreateResourceRequest,
- githubUrl,
- setGithubUrl,
-}: {
+ nextStage,
+ createResourceRequest,
+ setCreateResourceRequest,
+ githubUrl,
+ setGithubUrl,
+ }: {
nextStage: () => void;
createResourceRequest: CreateResourceRequest;
setCreateResourceRequest: (data: CreateResourceRequest) => void;
@@ -65,28 +65,34 @@ export const AddGitUrl = ({
};
return (
- <VStack alignItems="flex-start">
- <Text>Paste GitHub Url</Text>
- <Text fontSize="sm" color="gray.500">
- We'll pull the <Code>cybershuttle.yml</Code> file from the
- repository to auto-populate the project fields.
- </Text>
- <Input
- placeholder="https://github.com/username/repo.git"
- value={githubUrl}
- onChange={(e) => setGithubUrl(e.target.value)}
- mt={2}
- />
- <Button
- width="full"
- loading={loadingPull}
- onClick={onPullCybershuttleYml}
- mt={4}
- colorScheme="blue"
- disabled={!githubUrl}
- >
- Pull cybershuttle.yml file
- </Button>
- </VStack>
+ <VStack alignItems="flex-start">
+ <Text>Paste GitHub Url</Text>
+ <Text fontSize="sm" color="gray.500">
+ We'll pull the <Code>cybershuttle.yml</Code> file from the
+ repository to auto-populate the project fields.
+ </Text>
+ <Input
+ placeholder="https://github.com/username/repo"
+ value={githubUrl}
+ onChange={(e) => {
+ let githubUrl = e.target.value;
+ if (githubUrl.endsWith(".git")) {
+ githubUrl = githubUrl.replace(".git", "");
+ }
+ setGithubUrl(githubUrl);
+ }}
+ mt={2}
+ />
+ <Button
+ width="full"
+ loading={loadingPull}
+ onClick={onPullCybershuttleYml}
+ mt={4}
+ colorScheme="blue"
+ disabled={!githubUrl}
+ >
+ Pull cybershuttle.yml file
+ </Button>
+ </VStack>
);
};
diff --git
a/modules/research-framework/portal/src/components/add/AddProjectMaster.tsx
b/modules/research-framework/portal/src/components/add/AddProjectMaster.tsx
index f28f2d85b0..2241c10c79 100644
--- a/modules/research-framework/portal/src/components/add/AddProjectMaster.tsx
+++ b/modules/research-framework/portal/src/components/add/AddProjectMaster.tsx
@@ -1,27 +1,19 @@
-import {
- Box,
- Button,
- Container,
- Field,
- Heading,
- Input,
- VStack,
-} from "@chakra-ui/react";
-import { useState } from "react";
-import { useNavigate } from "react-router";
-import { FaArrowLeft } from "react-icons/fa";
-import { CreateProjectRequest } from
"@/interfaces/Requests/CreateProjectRequest";
+import {Box, Button, Container, Field, Heading, Input, VStack,} from
"@chakra-ui/react";
+import {useState} from "react";
+import {useNavigate} from "react-router";
+import {FaArrowLeft} from "react-icons/fa";
+import {CreateProjectRequest} from
"@/interfaces/Requests/CreateProjectRequest";
import RepoSearchInput from "./RepoSearch";
-import { DatasetSearchInput } from "./DatasetSearch";
+import {DatasetSearchInput} from "./DatasetSearch";
import api from "@/lib/api";
-import { CONTROLLER } from "@/lib/controller";
-import { toaster } from "../ui/toaster";
-import { useAuth } from "react-oidc-context";
+import {CONTROLLER} from "@/lib/controller";
+import {toaster} from "../ui/toaster";
+import {useAuth} from "react-oidc-context";
export const AddProjectMaster = () => {
const navigate = useNavigate();
const [createProjectRequest, setCreateProjectRequest] = useState(
- {} as CreateProjectRequest
+ {} as CreateProjectRequest
);
const [loading, setLoading] = useState(false);
const auth = useAuth();
@@ -39,8 +31,8 @@ export const AddProjectMaster = () => {
return;
}
if (
- !createProjectRequest.name ||
- createProjectRequest.name.length === 0
+ !createProjectRequest.name ||
+ createProjectRequest.name.length === 0
) {
toaster.create({
title: "Error creating project",
@@ -49,8 +41,8 @@ export const AddProjectMaster = () => {
});
return;
} else if (
- !createProjectRequest.repositoryId ||
- createProjectRequest.repositoryId.length === 0
+ !createProjectRequest.repositoryId ||
+ createProjectRequest.repositoryId.length === 0
) {
toaster.create({
title: "Error creating project",
@@ -81,58 +73,57 @@ export const AddProjectMaster = () => {
};
return (
- <Container maxW="breakpoint-sm" mt={8}>
- <Box maxW="breakpoint-sm" mx="auto">
- <Heading
- textAlign="center"
- fontSize={{ base: "4xl", md: "5xl" }}
- fontWeight="black"
- lineHeight={1.2}
- >
- Add Project
- </Heading>
- <Button
- onClick={() => {
- navigate("/add");
- }}
- variant="ghost"
- p={0}
- mb={2}
- >
- <FaArrowLeft />
- Back
- </Button>
- </Box>
+ <Container maxW="breakpoint-sm" mt={8}>
+ <Box maxW="breakpoint-sm" mx="auto">
+ <Heading
+ textAlign="center"
+ fontSize={{base: "4xl", md: "5xl"}}
+ fontWeight="black"
+ lineHeight={1.2}
+ >
+ Add Project
+ </Heading>
+ <Button
+ onClick={() => {
+ navigate("/add");
+ }}
+ variant="ghost"
+ p={0}
+ mb={2}
+ >
+ <FaArrowLeft/>
+ Back
+ </Button>
+ </Box>
- <VStack gap={4} alignItems="flex-start">
- <Field.Root>
- <Field.Label>Project Name</Field.Label>
- <Input
- value={createProjectRequest.name}
- onChange={(e) => {
- setCreateProjectRequest({
- ...createProjectRequest,
- name: e.target.value,
- });
- }}
- placeholder="Enter project name"
- />
- </Field.Root>
+ <VStack gap={4} alignItems="flex-start">
+ <Field.Root>
+ <Field.Label>Project Name</Field.Label>
+ <Input
+ value={createProjectRequest.name}
+ onChange={(e) => {
+ setCreateProjectRequest({
+ ...createProjectRequest,
+ name: e.target.value,
+ });
+ }}
+ placeholder="Enter project name"
+ />
+ </Field.Root>
- <RepoSearchInput
- createResourceRequest={createProjectRequest}
- setCreateResourceRequest={setCreateProjectRequest}
- />
+ <RepoSearchInput
+ createResourceRequest={createProjectRequest}
+ setCreateResourceRequest={setCreateProjectRequest}
+ />
- <DatasetSearchInput
- createResourceRequest={createProjectRequest}
- setCreateResourceRequest={setCreateProjectRequest}
- />
+ <DatasetSearchInput
+ setCreateResourceRequest={setCreateProjectRequest}
+ />
- <Button onClick={handleSubmit} loading={loading} w="full">
- Submit
- </Button>
- </VStack>
- </Container>
+ <Button onClick={handleSubmit} loading={loading} w="full">
+ Submit
+ </Button>
+ </VStack>
+ </Container>
);
};
diff --git
a/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
b/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
index 5a8f8087d5..9df9f97bbc 100644
--- a/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
+++ b/modules/research-framework/portal/src/components/add/DatasetSearch.tsx
@@ -1,38 +1,28 @@
-import { CreateProjectRequest } from
"@/interfaces/Requests/CreateProjectRequest";
-import {
- VStack,
- Field,
- Input,
- Text,
- Spinner,
- Box,
- HStack,
-} from "@chakra-ui/react";
-import { LuSearch } from "react-icons/lu";
-import { InputGroup } from "../ui/input-group";
-import { SetStateAction, useEffect, useState } from "react";
-import { DatasetResource } from "@/interfaces/ResourceType";
-import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
-import { CONTROLLER } from "@/lib/controller";
+import {CreateProjectRequest} from
"@/interfaces/Requests/CreateProjectRequest";
+import {Box, Field, HStack, Input, Spinner, Text, VStack,} from
"@chakra-ui/react";
+import {LuSearch} from "react-icons/lu";
+import {InputGroup} from "../ui/input-group";
+import {SetStateAction, useEffect, useState} from "react";
+import {DatasetResource} from "@/interfaces/ResourceType";
+import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
+import {CONTROLLER} from "@/lib/controller";
import api from "@/lib/api";
-import { FaCheck } from "react-icons/fa";
+import {FaCheck} from "react-icons/fa";
export const DatasetSearchInput = ({
- setCreateResourceRequest,
- createResourceRequest,
-}: {
+ setCreateResourceRequest,
+ }: {
setCreateResourceRequest: (
- data: SetStateAction<CreateProjectRequest>
+ data: SetStateAction<CreateProjectRequest>
) => void;
- createResourceRequest: CreateProjectRequest;
}) => {
const [datasetSearch, setDatasetSearch] = useState("");
const [debounceTimeout, setDebounceTimeout] = useState<NodeJS.Timeout |
null>(
- null
+ null
);
const [selectedDatasets, setSelectedDatasets] = useState<DatasetResource[]>(
- []
+ []
);
const [results, setResults] = useState<DatasetResource[]>([]);
const [loading, setLoading] = useState(false);
@@ -78,88 +68,88 @@ export const DatasetSearchInput = ({
}, [selectedDatasets]);
return (
- <VStack align="start" width="full" gap={2}>
- <Field.Root>
- <Field.Label>Dataset (select multiple)</Field.Label>
- <InputGroup startElement={<LuSearch />} w="full">
- <Input
- value={datasetSearch}
- onChange={(e) => setDatasetSearch(e.target.value)}
- placeholder="Enter dataset name"
- />
- </InputGroup>
+ <VStack align="start" width="full" gap={2}>
+ <Field.Root>
+ <Field.Label>Dataset (select multiple)</Field.Label>
+ <InputGroup startElement={<LuSearch/>} w="full">
+ <Input
+ value={datasetSearch}
+ onChange={(e) => setDatasetSearch(e.target.value)}
+ placeholder="Enter dataset name"
+ />
+ </InputGroup>
- {selectedDatasets.length > 0 && (
- <Box mt={2} w="full">
- <HStack gap={1}>
- {selectedDatasets.map((res) => {
- return (
- <Box
- key={res.id}
- borderWidth={1}
- borderRadius="md"
- p={2}
- cursor={"pointer"}
- _hover={{ bg: "red.200" }}
- onClick={() => {
- setSelectedDatasets((prev) =>
- prev.filter((item) => item.id !== res.id)
- );
- }}
- >
- <HStack>
- <FaCheck />
- <Text key={res.id} fontSize="sm">
- {res.name}
- </Text>
- </HStack>
- </Box>
- );
- })}
- </HStack>
- </Box>
- )}
+ {selectedDatasets.length > 0 && (
+ <Box mt={2} w="full">
+ <HStack gap={1}>
+ {selectedDatasets.map((res) => {
+ return (
+ <Box
+ key={res.id}
+ borderWidth={1}
+ borderRadius="md"
+ p={2}
+ cursor={"pointer"}
+ _hover={{bg: "red.200"}}
+ onClick={() => {
+ setSelectedDatasets((prev) =>
+ prev.filter((item) => item.id !== res.id)
+ );
+ }}
+ >
+ <HStack>
+ <FaCheck/>
+ <Text key={res.id} fontSize="sm">
+ {res.name}
+ </Text>
+ </HStack>
+ </Box>
+ );
+ })}
+ </HStack>
+ </Box>
+ )}
- {loading && <Spinner size="sm" />}
+ {loading && <Spinner size="sm"/>}
- {!loading && results.length > 0 && (
- <Box mt={2} w="full">
- <VStack align="start" gap={1}>
- {results.map((res) => {
- const isSelected = selectedDatasets.some(
- (item) => item.id === res.id
- );
+ {!loading && results.length > 0 && (
+ <Box mt={2} w="full">
+ <VStack align="start" gap={1}>
+ {results.map((res) => {
+ const isSelected = selectedDatasets.some(
+ (item) => item.id === res.id
+ );
- return (
- <Box
- key={res.id}
- bg={isSelected ? "green.200" : "white"}
- borderWidth={1}
- borderRadius="md"
- p={2}
- w="full"
- cursor={"pointer"}
- _hover={{ bg: "gray.100" }}
- onClick={() => {
- setSelectedDatasets((prev) => {
- if (isSelected) {
- return prev.filter((item) => item.id !== res.id);
- } else {
- return [...prev, res];
- }
- });
- }}
- >
- <Text key={res.id} fontSize="sm">
- {res.name}
- </Text>
- </Box>
- );
- })}
- </VStack>
- </Box>
- )}
- </Field.Root>
- </VStack>
+ return (
+ <Box
+ key={res.id}
+ bg={isSelected ? "green.200" : "white"}
+ borderWidth={1}
+ borderRadius="md"
+ p={2}
+ w="full"
+ cursor={"pointer"}
+ _hover={{bg: "gray.100"}}
+ onClick={() => {
+ setSelectedDatasets((prev) => {
+ if (isSelected) {
+ return prev.filter((item) => item.id !==
res.id);
+ } else {
+ return [...prev, res];
+ }
+ });
+ }}
+ >
+ <Text key={res.id} fontSize="sm">
+ {res.name}
+ </Text>
+ </Box>
+ );
+ })}
+ </VStack>
+ </Box>
+ )}
+ </Field.Root>
+ </VStack>
);
};
diff --git
a/modules/research-framework/portal/src/components/add/RepoSearch.tsx
b/modules/research-framework/portal/src/components/add/RepoSearch.tsx
index 4d96a94b6d..3f588a5d14 100644
--- a/modules/research-framework/portal/src/components/add/RepoSearch.tsx
+++ b/modules/research-framework/portal/src/components/add/RepoSearch.tsx
@@ -1,30 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
-import { useEffect, useState } from "react";
-import {
- Box,
- Button,
- Field,
- HStack,
- Input,
- Spinner,
- Text,
- VStack,
-} from "@chakra-ui/react";
-import { LuSearch } from "react-icons/lu";
-import { CreateProjectRequest } from
"@/interfaces/Requests/CreateProjectRequest";
-import { InputGroup } from "../ui/input-group";
-import { CONTROLLER } from "@/lib/controller";
+import {useEffect, useState} from "react";
+import {Box, Button, Field, HStack, Input, Spinner, Text, VStack,} from
"@chakra-ui/react";
+import {LuSearch} from "react-icons/lu";
+import {CreateProjectRequest} from
"@/interfaces/Requests/CreateProjectRequest";
+import {InputGroup} from "../ui/input-group";
+import {CONTROLLER} from "@/lib/controller";
import api from "@/lib/api";
-import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
-import { RepositoryResource } from "@/interfaces/ResourceType";
-import { ResourceCard } from "../home/ResourceCard";
+import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
+import {RepositoryResource} from "@/interfaces/ResourceType";
+import {ResourceCard} from "../home/ResourceCard";
const RepoSearchInput = ({
- setCreateResourceRequest,
- createResourceRequest,
-}: {
+ setCreateResourceRequest,
+ createResourceRequest,
+ }: {
setCreateResourceRequest: (data: CreateProjectRequest) => void;
createResourceRequest: CreateProjectRequest;
}) => {
@@ -32,10 +23,10 @@ const RepoSearchInput = ({
const [results, setResults] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [debounceTimeout, setDebounceTimeout] = useState<NodeJS.Timeout |
null>(
- null
+ null
);
const [selectedRepo, setSelectedRepo] = useState<RepositoryResource | null>(
- null
+ null
);
useEffect(() => {
@@ -72,85 +63,85 @@ const RepoSearchInput = ({
if (selectedRepo) {
return (
- <>
- <Field.Root>
- <HStack>
- <Field.Label>Repository</Field.Label>
- <Button
- size="xs"
- colorPalette="red"
- variant="outline"
- onClick={() => {
- setSelectedRepo(null);
- setCreateResourceRequest({
- ...createResourceRequest,
- repositoryId: "",
- });
- setRepoSearch("");
- }}
- >
- Reset Selection
- </Button>
- </HStack>
- </Field.Root>
+ <>
+ <Field.Root>
+ <HStack>
+ <Field.Label>Repository</Field.Label>
+ <Button
+ size="xs"
+ colorPalette="red"
+ variant="outline"
+ onClick={() => {
+ setSelectedRepo(null);
+ setCreateResourceRequest({
+ ...createResourceRequest,
+ repositoryId: "",
+ });
+ setRepoSearch("");
+ }}
+ >
+ Reset Selection
+ </Button>
+ </HStack>
+ </Field.Root>
- <ResourceCard size="sm" clickable={false} resource={selectedRepo} />
- </>
+ <ResourceCard size="sm" resource={selectedRepo} deletable={false}/>
+ </>
);
}
return (
- <VStack align="start" width="full" gap={2}>
- <Field.Root>
- <Field.Label>Repository</Field.Label>
- <InputGroup startElement={<LuSearch />} w="full">
- <Input
- value={repoSearch}
- onChange={(e) => setRepoSearch(e.target.value)}
- placeholder="Enter repository name"
- />
- </InputGroup>
- </Field.Root>
+ <VStack align="start" width="full" gap={2}>
+ <Field.Root>
+ <Field.Label>Repository</Field.Label>
+ <InputGroup startElement={<LuSearch/>} w="full">
+ <Input
+ value={repoSearch}
+ onChange={(e) => setRepoSearch(e.target.value)}
+ placeholder="Enter repository name"
+ />
+ </InputGroup>
+ </Field.Root>
- {loading && <Spinner size="sm" />}
+ {loading && <Spinner size="sm"/>}
- {!loading && results.length > 0 && (
- <Box mt={2} w="full">
- <VStack align="start" gap={1}>
- {results.map((res) => {
- return (
- <Box
- key={res.id}
- borderWidth={1}
- borderRadius="md"
- p={2}
- w="full"
- cursor={"pointer"}
- _hover={{ bg: "gray.100" }}
- onClick={() => {
- setSelectedRepo(res);
- setCreateResourceRequest({
- ...createResourceRequest,
- repositoryId: res.id,
- });
- }}
- >
- <Text key={res.id} fontSize="sm">
- {res.name}
- </Text>
- </Box>
- );
- })}
- </VStack>
- </Box>
- )}
+ {!loading && results.length > 0 && (
+ <Box mt={2} w="full">
+ <VStack align="start" gap={1}>
+ {results.map((res) => {
+ return (
+ <Box
+ key={res.id}
+ borderWidth={1}
+ borderRadius="md"
+ p={2}
+ w="full"
+ cursor={"pointer"}
+ _hover={{bg: "gray.100"}}
+ onClick={() => {
+ setSelectedRepo(res);
+ setCreateResourceRequest({
+ ...createResourceRequest,
+ repositoryId: res.id,
+ });
+ }}
+ >
+ <Text key={res.id} fontSize="sm">
+ {res.name}
+ </Text>
+ </Box>
+ );
+ })}
+ </VStack>
+ </Box>
+ )}
- {!loading && results.length === 0 && repoSearch !== "" && (
- <Text fontSize="sm" color="gray.500">
- No repositories found
- </Text>
- )}
- </VStack>
+ {!loading && results.length === 0 && repoSearch !== "" && (
+ <Text fontSize="sm" color="gray.500">
+ No repositories found
+ </Text>
+ )}
+ </VStack>
);
};
diff --git
a/modules/research-framework/portal/src/components/home/ProjectCard.tsx
b/modules/research-framework/portal/src/components/home/ProjectCard.tsx
index d11ce5c968..243718b59f 100644
--- a/modules/research-framework/portal/src/components/home/ProjectCard.tsx
+++ b/modules/research-framework/portal/src/components/home/ProjectCard.tsx
@@ -1,34 +1,46 @@
-import { ProjectType } from "@/interfaces/ProjectType";
-import { Card, HStack, Text } from "@chakra-ui/react";
-import { StartSessionFromProjectButton } from
"./StartSessionFromProjectButton";
+import {ProjectType} from "@/interfaces/ProjectType";
+import {Card, HStack, Text} from "@chakra-ui/react";
+import {StartSessionFromProjectButton} from "./StartSessionFromProjectButton";
+import {DeleteProjectButton} from
"@/components/projects/DeleteProjectButton.tsx";
+import {useState} from "react";
+
+export const ProjectCard = ({project}: { project: ProjectType }) => {
+ const [hideCard, setHideCard] = useState(false);
+
+ const onDeleteSuccess = () => {
+ setHideCard(true);
+ }
-export const ProjectCard = ({ project }: { project: ProjectType }) => {
return (
- <Card.Root overflow="hidden" size="md" height="fit-content">
- <Card.Body gap="2">
- {/* Card Content */}
- <HStack alignItems="center" justifyContent="space-between">
- <Card.Title>{project.name}</Card.Title>
- <Text color="gray.500">
- {new Date(project.createdAt).toLocaleDateString()}
- </Text>
- </HStack>
+ <Card.Root overflow="hidden" size="md" height="fit-content"
hidden={hideCard}>
+ <Card.Body gap="2">
+ {/* Card Content */}
+ <HStack alignItems="flex-start" justifyContent="space-between">
+ <Card.Title>
+ {project.name}
+ <Text color="gray.500" fontSize={'sm'}>
+ {new Date(project.createdAt).toLocaleDateString()}
+ </Text>
+ </Card.Title>
+
+ <DeleteProjectButton project={project}
onSuccess={onDeleteSuccess}/>
+ </HStack>
- <Text fontWeight="bold">
- Repository:{" "}
- <Text as="span" fontWeight="normal">
- {project.repositoryResource.name}
+ <Text fontWeight="bold">
+ Repository:{" "}
+ <Text as="span" fontWeight="normal">
+ {project.repositoryResource.name}
+ </Text>
</Text>
- </Text>
- <Text fontWeight="bold">
- Datasets:{" "}
- <Text as="span" fontWeight="normal">
- {project.datasetResources.map((dataset) => dataset.name).join(",
")}
+ <Text fontWeight="bold">
+ Datasets:{" "}
+ <Text as="span" fontWeight="normal">
+ {project.datasetResources.map((dataset) => dataset.name).join(",
")}
+ </Text>
</Text>
- </Text>
- <StartSessionFromProjectButton project={project} />
- </Card.Body>
- </Card.Root>
+ <StartSessionFromProjectButton project={project}/>
+ </Card.Body>
+ </Card.Root>
);
};
diff --git
a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
index 7fe9b7064d..047ab3aa5e 100644
--- a/modules/research-framework/portal/src/components/home/ResourceCard.tsx
+++ b/modules/research-framework/portal/src/components/home/ResourceCard.tsx
@@ -2,49 +2,50 @@ import {ModelResource, Resource} from
"@/interfaces/ResourceType";
import {Tag} from "@/interfaces/TagType";
import {isValidImaage, resourceTypeToColor} from "@/lib/util";
import {Avatar, Badge, Box, Card, HStack, Image, Text,} from
"@chakra-ui/react";
-import {Link} from "react-router";
import {ResourceTypeBadge} from "../resources/ResourceTypeBadge";
import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
import {ModelCardButton} from "../models/ModelCardButton";
+import {DeleteResourceButton} from
"@/components/resources/DeleteResourceButton.tsx";
+import {useState} from "react";
+import {Link} from 'react-router';
export const ResourceCard = ({
resource,
- appendTypeToUrl = false,
- clickable = true,
size = "sm",
+ deletable = true
}: {
resource: Resource;
- appendTypeToUrl?: boolean;
- clickable?: boolean;
size?: "sm" | "md" | "lg";
+ deletable?: boolean
}) => {
+ const [hideCard, setHideCard] = useState(false);
const author = resource.authors[0];
const isValidImage = isValidImaage(resource.headerImage);
resource.tags.sort((a, b) => a.value.localeCompare(b.value));
- const linkTo = resource.id;
const linkToWithType = `${resource.type}/${resource.id}`;
- // Determine the link based on appendTypeToUrl
- const link = appendTypeToUrl ? linkToWithType : linkTo;
+ const link = '/resources/' + linkToWithType;
+ const onDeleteSuccess = () => {
+ setHideCard(true);
+ }
+
const content = (
<Card.Root
overflow="hidden"
size={size}
- _hover={{bg: resourceTypeToColor(resource.type) + ".100"}}
>
{isValidImage && (
<Box position="relative" width="full">
- {/* Badge in the upper-left */}
<ResourceTypeBadge
type={resource.type}
position="absolute"
top="2"
left="2"
zIndex="1"
- boxShadow="md" // Optional: Shadow for visibility
+ boxShadow="md"
/>
{/* Full-width Image */}
@@ -58,58 +59,71 @@ export const ResourceCard = ({
</Box>
)}
- <Card.Body gap="2">
- {/* Card Content */}
- <Card.Title>{resource.name}</Card.Title>
- {!isValidImage && (
- <Box>
- <ResourceTypeBadge type={resource.type}/>
- </Box>
- )}
- <HStack flexWrap="wrap">
- {resource.tags.map((tag: Tag) => (
- <Badge
- key={tag.id}
- size="md"
- rounded="md"
- colorPalette={resourceTypeToColor(resource.type)}
- >
- {tag.value}
- </Badge>
- ))}
+ <Card.Header>
+ <HStack justifyContent={'space-between'} alignItems={'center'}
flexWrap={'wrap'}>
+ <Card.Title>{resource.name}</Card.Title>
+ {deletable && <DeleteResourceButton resource={resource}
onSuccess={onDeleteSuccess}/>}
</HStack>
- <Text color="fg.muted" lineClamp={2}>
- {resource.description}
- </Text>
- </Card.Body>
+ </Card.Header>
- <Card.Footer justifyContent="space-between" pt={4}>
- {author && (
- <HStack>
- <Avatar.Root shape="full" size="sm">
- <Avatar.Fallback name={author}/>
- <Avatar.Image src={author}/>
- </Avatar.Root>
+ <Link to={link} target={"_blank"}>
+ <Box
+ _hover={{bg: resourceTypeToColor(resource.type) + ".100"}}
+ >
+ <Card.Body gap="2">
+ {!isValidImage && (
+ <Box>
+ <ResourceTypeBadge type={resource.type}/>
+ </Box>
+ )}
- <Box>
- <Text fontWeight="bold">{author}</Text>
- </Box>
+ {/* Card Content */}
+ <HStack flexWrap="wrap">
+ {resource.tags.map((tag: Tag) => (
+ <Badge
+ key={tag.id}
+ size="md"
+ rounded="md"
+ colorPalette={resourceTypeToColor(resource.type)}
+ >
+ {tag.value}
+ </Badge>
+ ))}
</HStack>
- )}
+ <Text color="fg.muted" lineClamp={2}>
+ {resource.description}
+ </Text>
+ </Card.Body>
+
+ <Card.Footer justifyContent="space-between" pt={4}>
+ {author && (
+ <HStack>
+ <Avatar.Root shape="full" size="sm">
+ <Avatar.Fallback name={author}/>
+ <Avatar.Image src={author}/>
+ </Avatar.Root>
- {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.MODEL && (
- <ModelCardButton model={resource as ModelResource}/>
- )}
- </Card.Footer>
+ <Box>
+ <Text fontWeight="bold">{author}</Text>
+ </Box>
+ </HStack>
+ )}
+
+ {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.MODEL
&& (
+ <ModelCardButton model={resource as ModelResource}/>
+ )}
+ </Card.Footer>
+ </Box>
+ </Link>
</Card.Root>
);
- if (clickable) {
- return (
- <Box>
- <Link to={link}>{content}</Link>
- </Box>
- );
- }
- return <Box>{content}</Box>;
+ // if (clickable) {
+ // return (
+ // <Box hidden={hideCard}>
+ // <Link to={link}>{content}</Link>
+ // </Box>
+ // );
+ // }
+ return <Box hidden={hideCard}>{content}</Box>;
};
diff --git
a/modules/research-framework/portal/src/components/notebooks/index.tsx
b/modules/research-framework/portal/src/components/notebooks/index.tsx
index cb241577ea..a7af2a508e 100644
--- a/modules/research-framework/portal/src/components/notebooks/index.tsx
+++ b/modules/research-framework/portal/src/components/notebooks/index.tsx
@@ -1,13 +1,13 @@
-import { Container, Input, SimpleGrid } from "@chakra-ui/react";
-import { PageHeader } from "../PageHeader";
-import { InputGroup } from "../ui/input-group";
-import { LuSearch } from "react-icons/lu";
-import { useEffect, useState } from "react";
+import {Container, Input, SimpleGrid} from "@chakra-ui/react";
+import {PageHeader} from "../PageHeader";
+import {InputGroup} from "../ui/input-group";
+import {LuSearch} from "react-icons/lu";
+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";
+import {NotebookResource} from "@/interfaces/ResourceType";
+import {ResourceCard} from "../home/ResourceCard";
+import {CONTROLLER} from "@/lib/controller";
+import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
const getNotebooks = async () => {
try {
@@ -38,34 +38,33 @@ const Notebooks = () => {
}, []);
return (
- <>
- <Container maxW="container.lg" mt={8}>
- <PageHeader
- title="Notebooks"
- description="Create and manage your notebooks. From here, you can
create new notebooks, view existing ones, and manage them."
- />
- <InputGroup endElement={<LuSearch />} w="100%" mt={4}>
- <Input placeholder="Search" rounded="md" />
- </InputGroup>
+ <>
+ <Container maxW="container.lg" mt={8}>
+ <PageHeader
+ title="Notebooks"
+ description="Create and manage your notebooks. From here, you
can create new notebooks, view existing ones, and manage them."
+ />
+ <InputGroup endElement={<LuSearch/>} w="100%" mt={4}>
+ <Input placeholder="Search" rounded="md"/>
+ </InputGroup>
- <SimpleGrid
- columns={{ base: 1, md: 2, lg: 3 }}
- mt={4}
- gap={12}
- justifyContent="space-around"
- >
- {notebooks.map((notebook: NotebookResource) => {
- return (
- <ResourceCard
- resource={notebook}
- key={notebook.id}
- appendTypeToUrl={false}
- />
- );
- })}
- </SimpleGrid>
- </Container>
- </>
+ <SimpleGrid
+ columns={{base: 1, md: 2, lg: 3}}
+ mt={4}
+ gap={12}
+ justifyContent="space-around"
+ >
+ {notebooks.map((notebook: NotebookResource) => {
+ return (
+ <ResourceCard
+ resource={notebook}
+ key={notebook.id}
+ />
+ );
+ })}
+ </SimpleGrid>
+ </Container>
+ </>
);
};
diff --git
a/modules/research-framework/portal/src/components/projects/DeleteProjectButton.tsx
b/modules/research-framework/portal/src/components/projects/DeleteProjectButton.tsx
new file mode 100644
index 0000000000..a9621d462c
--- /dev/null
+++
b/modules/research-framework/portal/src/components/projects/DeleteProjectButton.tsx
@@ -0,0 +1,115 @@
+import {Box, Button, CloseButton, Dialog, Input, Menu, Portal, Text,
useDialog} from "@chakra-ui/react";
+import {BsThreeDots} from "react-icons/bs";
+import {FaTrash} from "react-icons/fa";
+import {useAuth} from "react-oidc-context";
+import {isProjectOwner} from "@/lib/util.ts";
+import {useState} from "react";
+import api from "@/lib/api.ts";
+import {CONTROLLER} from "@/lib/controller.ts";
+import {toaster} from "@/components/ui/toaster.tsx";
+import {ProjectType} from "@/interfaces/ProjectType.tsx";
+
+export const DeleteProjectButton = ({
+ project,
+ onSuccess,
+ }: {
+ project: ProjectType
+ onSuccess: () => void;
+}) => {
+ const dialog = useDialog();
+ const [deleteName, setDeleteName] = useState("");
+ const [deleteLoading, setDeleteLoading] = useState(false);
+ const auth = useAuth();
+ const isOwner = isProjectOwner(auth.user?.profile.email || "INVALID",
project);
+ if (!isOwner || !auth.isAuthenticated) {
+ return null;
+ }
+
+ const handleDeleteProject = async () => {
+ setDeleteLoading(true);
+ try {
+ await api.delete(`${CONTROLLER.projects}/${project.id}`);
+ toaster.create({
+ title: "Project deleted",
+ description: project.name,
+ type: "success",
+ })
+ onSuccess();
+ dialog.setOpen(false);
+ } catch {
+ toaster.create({
+ title: "Error deleting session",
+ type: "error",
+ });
+ } finally {
+ setDeleteLoading(false)
+ }
+ }
+
+ return (
+ <>
+ <Menu.Root>
+ <Menu.Trigger _hover={{
+ cursor: 'pointer',
+ }}>
+ <BsThreeDots/>
+ </Menu.Trigger>
+ <Menu.Positioner>
+ <Menu.Content>
+ <Menu.Item value={"delete"}
+ color="fg.error"
+ _hover={{bg: "bg.error", color: "fg.error", cursor:
"pointer"}}
+ onClick={() => dialog.setOpen(true)}
+ >
+ <FaTrash/>
+ <Box flex="1">Delete</Box>
+
+ </Menu.Item>
+
+ </Menu.Content>
+ </Menu.Positioner>
+ </Menu.Root>
+
+ <Dialog.RootProvider size="sm" value={dialog}>
+ <Portal>
+ <Dialog.Backdrop/>
+ <Dialog.Positioner>
+ <Dialog.Content>
+ <Dialog.Header>
+ <Dialog.Title>Delete Project</Dialog.Title>
+ </Dialog.Header>
+ <Dialog.Body>
+ <Text color="gray.500">
+ This action is irreversible. To confirm, please type:{" "}
+ <b>{project.name}</b>.
+ </Text>
+
+ <Input
+ mt={2}
+ placeholder="Project name"
+ value={deleteName}
+ onChange={(e) => setDeleteName(e.target.value)}
+ />
+ </Dialog.Body>
+ <Dialog.Footer>
+ <Button
+ width="100%"
+ colorPalette="red"
+ disabled={deleteName !== project.name || deleteLoading}
+ loading={deleteLoading}
+ onClick={handleDeleteProject}
+ >
+ Delete
+ </Button>
+ </Dialog.Footer>
+ <Dialog.CloseTrigger asChild>
+ <CloseButton size="sm"/>
+ </Dialog.CloseTrigger>
+ </Dialog.Content>
+ </Dialog.Positioner>
+ </Portal>
+ </Dialog.RootProvider>
+
+ </>
+ )
+}
\ No newline at end of file
diff --git
a/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
b/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
new file mode 100644
index 0000000000..580c4f5051
--- /dev/null
+++
b/modules/research-framework/portal/src/components/resources/DeleteResourceButton.tsx
@@ -0,0 +1,115 @@
+import {Box, Button, CloseButton, Dialog, Input, Menu, Portal, Text,
useDialog} from "@chakra-ui/react";
+import {BsThreeDots} from "react-icons/bs";
+import {FaTrash} from "react-icons/fa";
+import {Resource} from "@/interfaces/ResourceType.ts";
+import {useAuth} from "react-oidc-context";
+import {isResourceOwner} from "@/lib/util.ts";
+import {useState} from "react";
+import api from "@/lib/api.ts";
+import {CONTROLLER} from "@/lib/controller.ts";
+import {toaster} from "@/components/ui/toaster.tsx";
+
+export const DeleteResourceButton = ({
+ resource,
+ onSuccess,
+ }: {
+ resource: Resource
+ onSuccess: () => void;
+}) => {
+ const dialog = useDialog();
+ const [deleteName, setDeleteName] = useState("");
+ const [deleteLoading, setDeleteLoading] = useState(false);
+ const auth = useAuth();
+ const isOwner = isResourceOwner(auth.user?.profile.email || "INVALID",
resource);
+ if (!isOwner || !auth.isAuthenticated) {
+ return null;
+ }
+
+ const handleDeleteResource = async () => {
+ setDeleteLoading(true);
+ try {
+ await api.delete(`${CONTROLLER.resources}/${resource.id}`);
+ toaster.create({
+ title: "Resource deleted",
+ description: resource.name,
+ type: "success",
+ })
+ onSuccess();
+ dialog.setOpen(false);
+ } catch {
+ toaster.create({
+ title: "Error deleting session",
+ type: "error",
+ });
+ } finally {
+ setDeleteLoading(false)
+ }
+ }
+
+ return (
+ <>
+ <Menu.Root>
+ <Menu.Trigger _hover={{
+ cursor: 'pointer',
+ }}>
+ <BsThreeDots/>
+ </Menu.Trigger>
+ <Menu.Positioner>
+ <Menu.Content>
+ <Menu.Item value={"delete"}
+ color="fg.error"
+ _hover={{bg: "bg.error", color: "fg.error", cursor:
"pointer"}}
+ onClick={() => dialog.setOpen(true)}
+ >
+ <FaTrash/>
+ <Box flex="1">Delete</Box>
+
+ </Menu.Item>
+
+ </Menu.Content>
+ </Menu.Positioner>
+ </Menu.Root>
+
+ <Dialog.RootProvider size="sm" value={dialog}>
+ <Portal>
+ <Dialog.Backdrop/>
+ <Dialog.Positioner>
+ <Dialog.Content>
+ <Dialog.Header>
+ <Dialog.Title>Delete Resource</Dialog.Title>
+ </Dialog.Header>
+ <Dialog.Body>
+ <Text color="gray.500">
+ This action is irreversible. To confirm, please type:{" "}
+ <b>{resource.name}</b>.
+ </Text>
+
+ <Input
+ mt={2}
+ placeholder="Resource name"
+ value={deleteName}
+ onChange={(e) => setDeleteName(e.target.value)}
+ />
+ </Dialog.Body>
+ <Dialog.Footer>
+ <Button
+ width="100%"
+ colorPalette="red"
+ disabled={deleteName !== resource.name || deleteLoading}
+ loading={deleteLoading}
+ onClick={handleDeleteResource}
+ >
+ Delete
+ </Button>
+ </Dialog.Footer>
+ <Dialog.CloseTrigger asChild>
+ <CloseButton size="sm"/>
+ </Dialog.CloseTrigger>
+ </Dialog.Content>
+ </Dialog.Positioner>
+ </Portal>
+ </Dialog.RootProvider>
+
+ </>
+ )
+}
\ No newline at end of file
diff --git
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
index feb844ee8e..80c5b17712 100644
---
a/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
+++
b/modules/research-framework/portal/src/components/resources/ResourceDetails.tsx
@@ -1,20 +1,20 @@
-import { useLocation, useNavigate, useParams } from "react-router";
+import {useLocation, useNavigate, useParams} from "react-router";
import {
+ Avatar,
+ Badge,
+ Box,
+ Button,
Container,
- Spinner,
+ Heading,
HStack,
- Box,
- Separator,
Icon,
- Text,
Image,
- Badge,
- Heading,
- Avatar,
- Button,
+ Separator,
+ Spinner,
+ Text,
} from "@chakra-ui/react";
-import { useEffect, useState } from "react";
-import { BiArrowBack } from "react-icons/bi";
+import {useEffect, useState} from "react";
+import {BiArrowBack} from "react-icons/bi";
import api from "@/lib/api";
import {
DatasetResource,
@@ -23,15 +23,16 @@ import {
RepositoryResource,
Resource,
} from "@/interfaces/ResourceType";
-import { Tag } from "@/interfaces/TagType";
-import { isValidImaage, resourceTypeToColor } from "@/lib/util";
-import { ResourceTypeBadge } from "./ResourceTypeBadge";
-import { ResourceTypeEnum } from "@/interfaces/ResourceTypeEnum";
-import { ModelSpecificBox } from "../models/ModelSpecificBox";
-import { NotebookSpecificDetails } from "../notebooks/NotebookSpecificDetails";
-import { RepositorySpecificDetails } from
"../repositories/RepositorySpecificDetails";
-import { CONTROLLER } from "@/lib/controller";
-import { DatasetSpecificDetails } from "../datasets/DatasetSpecificDetails";
+import {Tag} from "@/interfaces/TagType";
+import {isValidImaage, resourceTypeToColor} from "@/lib/util";
+import {ResourceTypeBadge} from "./ResourceTypeBadge";
+import {ResourceTypeEnum} from "@/interfaces/ResourceTypeEnum";
+import {ModelSpecificBox} from "../models/ModelSpecificBox";
+import {NotebookSpecificDetails} from "../notebooks/NotebookSpecificDetails";
+import {RepositorySpecificDetails} from
"../repositories/RepositorySpecificDetails";
+import {CONTROLLER} from "@/lib/controller";
+import {DatasetSpecificDetails} from "../datasets/DatasetSpecificDetails";
+import {DeleteResourceButton} from
"@/components/resources/DeleteResourceButton.tsx";
async function getResource(id: string) {
const response = await api.get(`${CONTROLLER.resources}/${id}`);
@@ -39,10 +40,10 @@ async function getResource(id: string) {
}
const ResourceDetails = () => {
- const { id } = useParams();
+ const {id} = useParams();
const [resource, setResource] = useState<Resource | null>(null);
const navigate = useNavigate();
- const { state } = useLocation();
+ const {state} = useLocation();
useEffect(() => {
if (!id) return;
@@ -53,133 +54,142 @@ const ResourceDetails = () => {
setResource(r);
}
+
getData();
}, [id, state]);
- if (!resource) return <Spinner />;
+ if (!resource) return <Spinner/>;
const validImage = isValidImaage(resource.headerImage);
+ const goToResources = () => {
+ navigate(
+ "/resources?resourceTypes=REPOSITORY%2CNOTEBOOK%2CDATASET%2CMODEL"
+ )
+ }
+
+
return (
- <>
- <Container maxW="breakpoint-lg" mx="auto" p={4} mt={16}>
- <Box>
- <Button
- variant="plain"
- p={0}
- onClick={() =>
- navigate(
-
"/resources?resourceTypes=REPOSITORY%2CNOTEBOOK%2CDATASET%2CMODEL"
- )
- }
- >
- <HStack
- alignItems="center"
- mb={4}
- display="inline-flex"
- _hover={{
- bg: "gray.300",
- }}
- p={1}
- rounded="md"
+ <>
+ <Container maxW="breakpoint-lg" mx="auto" p={4} mt={16}>
+ <Box>
+ <Button
+ variant="plain"
+ p={0}
+ onClick={goToResources}
>
- <Icon>
- <BiArrowBack />
- </Icon>
- Back
- </HStack>
- </Button>
- </Box>
-
- <HStack
- alignItems={"flex-start"}
- mb={4}
- gap={8}
- justifyContent="space-between"
- >
+ <HStack
+ alignItems="center"
+ mb={4}
+ display="inline-flex"
+ _hover={{
+ bg: "gray.300",
+ }}
+ p={1}
+ rounded="md"
+ >
+ <Icon>
+ <BiArrowBack/>
+ </Icon>
+ Back
+ </HStack>
+ </Button>
+ </Box>
+
+ <HStack
+ alignItems={"flex-start"}
+ mb={4}
+ gap={8}
+ justifyContent="space-between"
+ >
+ <Box w={'full'}>
+ <ResourceTypeBadge type={resource.type}/>
+
+ <HStack mt={2} justifyContent={'space-between'}
alignItems={'center'} flexWrap={'wrap'}>
+ <Heading as="h1" size="4xl">
+ {resource.name}
+ </Heading>
+
+ <DeleteResourceButton resource={resource}
onSuccess={goToResources}/>
+ </HStack>
+
+ <HStack mt={2}>
+ {resource.tags.map((tag: Tag) => (
+ <Badge
+ key={tag.id}
+ size="lg"
+ rounded="md"
+ colorPalette={resourceTypeToColor(resource.type)}
+ >
+ {tag.value}
+ </Badge>
+ ))}
+ </HStack>
+
+ <HStack mt={8}>
+ {resource.authors.map((author: string) => {
+ return (
+ <HStack key={author}>
+ <Avatar.Root shape="full" size="xl">
+ <Avatar.Fallback name={author}/>
+ <Avatar.Image src={author}/>
+ </Avatar.Root>
+
+ <Box>
+ <Text fontWeight="bold">{author}</Text>
+ </Box>
+ </HStack>
+ );
+ })}
+ </HStack>
+ </Box>
+
+ <Box>
+ {validImage && (
+ <Image
+ src={resource.headerImage}
+ alt="Notebook Header"
+ rounded="md"
+ maxW="300px"
+ />
+ )}
+ </Box>
+ </HStack>
+
+ <Separator my={6}/>
<Box>
- <ResourceTypeBadge type={resource.type} />
- <Heading as="h1" size="4xl" mt={2}>
- {resource.name}
+ <Heading fontWeight="bold" size="2xl">
+ About
</Heading>
- <HStack mt={2}>
- {resource.tags.map((tag: Tag) => (
- <Badge
- key={tag.id}
- size="lg"
- rounded="md"
- colorPalette={resourceTypeToColor(resource.type)}
- >
- {tag.value}
- </Badge>
- ))}
- </HStack>
-
- <HStack mt={8}>
- {resource.authors.map((author: string) => {
- return (
- <HStack key={author}>
- <Avatar.Root shape="full" size="xl">
- <Avatar.Fallback name={author} />
- <Avatar.Image src={author} />
- </Avatar.Root>
-
- <Box>
- <Text fontWeight="bold">{author}</Text>
- </Box>
- </HStack>
- );
- })}
- </HStack>
+ <Text>{resource.description}</Text>
</Box>
+ <Separator my={8}/>
+
<Box>
- {validImage && (
- <Image
- src={resource.headerImage}
- alt="Notebook Header"
- rounded="md"
- maxW="300px"
- />
+ {(resource.type as ResourceTypeEnum) ===
+ ResourceTypeEnum.REPOSITORY && (
+ <RepositorySpecificDetails
+ repository={resource as RepositoryResource}
+ />
+ )}
+
+ {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.DATASET
&& (
+ <DatasetSpecificDetails dataset={resource as DatasetResource}/>
+ )}
+
+ {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.MODEL &&
(
+ <ModelSpecificBox model={resource as ModelResource}/>
)}
+
+ {(resource.type as ResourceTypeEnum) ===
+ ResourceTypeEnum.NOTEBOOK && (
+ <NotebookSpecificDetails notebook={resource as
NotebookResource}/>
+ )}
</Box>
- </HStack>
-
- <Separator my={6} />
- <Box>
- <Heading fontWeight="bold" size="2xl">
- About
- </Heading>
-
- <Text>{resource.description}</Text>
- </Box>
-
- <Separator my={8} />
-
- <Box>
- {(resource.type as ResourceTypeEnum) ===
- ResourceTypeEnum.REPOSITORY && (
- <RepositorySpecificDetails
- repository={resource as RepositoryResource}
- />
- )}
-
- {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.DATASET &&
(
- <DatasetSpecificDetails dataset={resource as DatasetResource} />
- )}
-
- {(resource.type as ResourceTypeEnum) === ResourceTypeEnum.MODEL && (
- <ModelSpecificBox model={resource as ModelResource} />
- )}
-
- {(resource.type as ResourceTypeEnum) ===
- ResourceTypeEnum.NOTEBOOK && (
- <NotebookSpecificDetails notebook={resource as NotebookResource} />
- )}
- </Box>
- </Container>
- </>
+ </Container>
+ </>
);
};
diff --git
a/modules/research-framework/portal/src/components/resources/ResourceTypeBadge.tsx
b/modules/research-framework/portal/src/components/resources/ResourceTypeBadge.tsx
index 49c7b8b0d9..9ca15e1316 100644
---
a/modules/research-framework/portal/src/components/resources/ResourceTypeBadge.tsx
+++
b/modules/research-framework/portal/src/components/resources/ResourceTypeBadge.tsx
@@ -1,26 +1,27 @@
-import { resourceTypeToColor } from "@/lib/util";
-import { Badge } from "@chakra-ui/react";
+import {resourceTypeToColor} from "@/lib/util";
+import {Badge} from "@chakra-ui/react";
interface ResourceTypeBadgeProps {
type: string;
+
[key: string]: string | number | boolean; // Specify a more specific type
for additional props
}
export const ResourceTypeBadge = ({
- type,
- ...props
-}: ResourceTypeBadgeProps) => {
+ type,
+ ...props
+ }: ResourceTypeBadgeProps) => {
return (
- <Badge
- colorPalette={resourceTypeToColor(type)}
- fontWeight="bold"
- size="sm"
- px="2"
- py="1"
- borderRadius="md"
- {...props}
- >
- {type}
- </Badge>
+ <Badge
+ colorPalette={resourceTypeToColor(type)}
+ fontWeight="bold"
+ size="xs"
+ px="2"
+ py="1"
+ borderRadius="md"
+ {...props}
+ >
+ {type}
+ </Badge>
);
};
diff --git
a/modules/research-framework/portal/src/components/resources/index.tsx
b/modules/research-framework/portal/src/components/resources/index.tsx
index 12c4efacbf..a86bd1b626 100644
--- a/modules/research-framework/portal/src/components/resources/index.tsx
+++ b/modules/research-framework/portal/src/components/resources/index.tsx
@@ -334,7 +334,6 @@ export const Resources = () => {
<ResourceCard
resource={resource}
key={resource.id}
- appendTypeToUrl={true}
/>
);
})}
diff --git a/modules/research-framework/portal/src/interfaces/ProjectType.tsx
b/modules/research-framework/portal/src/interfaces/ProjectType.tsx
index 94d0afb6d2..87a7c78774 100644
--- a/modules/research-framework/portal/src/interfaces/ProjectType.tsx
+++ b/modules/research-framework/portal/src/interfaces/ProjectType.tsx
@@ -1,4 +1,4 @@
-import { DatasetResource, RepositoryResource } from "./ResourceType";
+import {DatasetResource, RepositoryResource} from "./ResourceType";
export interface ProjectType {
id: string;
@@ -7,6 +7,7 @@ export interface ProjectType {
datasetResources: DatasetResource[];
createdAt: string;
updatedAt: string;
+ ownerId: string;
}
export interface ProjectPostData {
diff --git a/modules/research-framework/portal/src/lib/util.ts
b/modules/research-framework/portal/src/lib/util.ts
index bad255b39a..9cfb333adb 100644
--- a/modules/research-framework/portal/src/lib/util.ts
+++ b/modules/research-framework/portal/src/lib/util.ts
@@ -1,3 +1,6 @@
+import {Resource} from "@/interfaces/ResourceType.ts";
+import {ProjectType} from "@/interfaces/ProjectType.tsx";
+
export const resourceTypeToColor = (type: string) => {
if (type === "NOTEBOOK") {
return "blue";
@@ -26,7 +29,15 @@ export const getGithubOwnerAndRepo = (url: string) => {
if (match) {
const owner = match[1];
const repo = match[2].replace(/\.git$/, "");
- return { owner, repo };
+ return {owner, repo};
}
return null;
+}
+
+export const isResourceOwner = (userEmail: string, resource: Resource) => {
+ return resource.authors.includes(userEmail);
+}
+
+export const isProjectOwner = (userEmail: string, project: ProjectType) => {
+ return project.ownerId.toLowerCase() === userEmail.toLowerCase();
}
\ No newline at end of file
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
index f4a45cb5ba..0fcd374cf5 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/config/AuthzTokenFilter.java
@@ -59,11 +59,11 @@ public class AuthzTokenFilter extends OncePerRequestFilter {
if (request.getMethod().equalsIgnoreCase("POST")
|| request.getMethod().equalsIgnoreCase("PUT")
- || request.getMethod().equalsIgnoreCase("PATCH")) {
+ || request.getMethod().equalsIgnoreCase("PATCH")
+ || request.getMethod().equalsIgnoreCase("DELETE")) {
return false; // mutation requests should be authenticated
}
- // TODO: ensure that only GET requests do not need auth
return path.startsWith("/swagger")
|| path.startsWith("/v2/api-docs")
|| path.startsWith("/v3/api-docs")
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
index b36cba52f5..a29c37ced2 100644
---
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
@@ -27,7 +27,13 @@ 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.*;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/rf/projects")
@@ -54,4 +60,10 @@ public class ProjectController {
public ResponseEntity<Project> createProject(@RequestBody
CreateProjectRequest createProjectRequest) {
return
ResponseEntity.ok(projectHandler.createProject(createProjectRequest));
}
+
+ @DeleteMapping("/{projectId}")
+ @Operation(summary = "Delete project by id")
+ public ResponseEntity<Boolean> deleteProjectById(@PathVariable(value =
"projectId") String projectId) {
+ return ResponseEntity.ok(projectHandler.deleteProject(projectId));
+ }
}
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 06d414f91f..d5f2f696a1 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
@@ -40,6 +40,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -100,12 +101,18 @@ public class ResourceController {
return ResponseEntity.ok(resourceHandler.getAllTagsByPopularity());
}
- @Operation(summary = "Get dataset, notebook, or repository")
+ @Operation(summary = "Get dataset, notebook, repository, or model")
@GetMapping(value = "/{id}")
public ResponseEntity<Resource> getResource(@PathVariable(value = "id")
String id) {
return ResponseEntity.ok(resourceHandler.getResourceById(id));
}
+ @Operation(summary = "Delete dataset, notebook, repository, or model")
+ @DeleteMapping(value = "/{id}")
+ public ResponseEntity<Boolean> deleteResource(@PathVariable(value = "id")
String id) {
+ return ResponseEntity.ok(resourceHandler.deleteResourceById(id));
+ }
+
@Operation(summary = "Get all resources")
@GetMapping("/")
public ResponseEntity<Page<Resource>> getAllResources(
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/StateEnum.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/StateEnum.java
new file mode 100644
index 0000000000..07586eacdd
--- /dev/null
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/enums/StateEnum.java
@@ -0,0 +1,25 @@
+/**
+*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements. See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership. The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied. See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+package org.apache.airavata.research.service.enums;
+
+public enum StateEnum {
+ ACTIVE,
+ DELETED
+}
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 545a6f8938..9fca965303 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
@@ -27,6 +27,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import org.apache.airavata.research.service.dto.CreateProjectRequest;
import org.apache.airavata.research.service.enums.ResourceTypeEnum;
+import org.apache.airavata.research.service.enums.StateEnum;
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;
@@ -52,7 +53,7 @@ public class ProjectHandler {
}
public Project findProject(String projectId) {
- return projectRepository.findById(projectId).orElseThrow(() -> {
+ return projectRepository.findByIdAndState(projectId,
StateEnum.ACTIVE).orElseThrow(() -> {
LOGGER.error("Unable to find a Project with id: {}", projectId);
return new EntityNotFoundException("Unable to find a Project with
id: " + projectId);
});
@@ -95,26 +96,46 @@ public class ProjectHandler {
Set<DatasetResource> datasetResourcesSet = new
HashSet<>(datasetResourcesList);
project.setDatasetResources(datasetResourcesSet);
+ project.setState(StateEnum.ACTIVE);
projectRepository.save(project);
return project;
}
public List<Project> getAllProjects() {
- return projectRepository.findAll();
+ return projectRepository.findALlByState(StateEnum.ACTIVE);
}
public List<Project> getAllProjectsByOwnerId(String ownerId) {
- return projectRepository.findAllByOwnerIdOrderByCreatedAtDesc(ownerId);
+ return
projectRepository.findAllByOwnerIdAndStateOrderByCreatedAtDesc(ownerId,
StateEnum.ACTIVE);
+ }
+
+ public boolean deleteProject(String projectId) {
+ Optional<Project> optionalProject =
projectRepository.findByIdAndState(projectId, StateEnum.ACTIVE);
+ if (optionalProject.isEmpty()
+ || StateEnum.DELETED.equals(optionalProject.get().getState()))
{
+ throw new EntityNotFoundException("Unable to find a Project with
id: " + projectId);
+ }
+
+ Project project = optionalProject.get();
+ String userId = UserContext.userId();
+ if (!project.getOwnerId().equalsIgnoreCase(userId)) {
+ throw new RuntimeException(
+ String.format("User %s is not authorized to delete project
with id: %s", userId, projectId));
+ }
+
+ project.setState(StateEnum.DELETED);
+ projectRepository.save(project);
+ return true;
}
public List<Project> findProjectsWithRepository(RepositoryResource
repositoryResource) {
- return
projectRepository.findProjectsByRepositoryResource(repositoryResource);
+ return
projectRepository.findProjectsByRepositoryResourceAndState(repositoryResource,
StateEnum.ACTIVE);
}
public List<Project> findProjectsContainingDataset(DatasetResource
datasetResource) {
Set<DatasetResource> set = new HashSet<>();
set.add(datasetResource);
- return projectRepository.findProjectsByDatasetResourcesContaining(set);
+ return
projectRepository.findProjectsByDatasetResourcesContainingAndState(set,
StateEnum.ACTIVE);
}
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
index 7de2ff38b1..475e2b7c18 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/handlers/ResourceHandler.java
@@ -19,16 +19,19 @@
*/
package org.apache.airavata.research.service.handlers;
+import jakarta.persistence.EntityNotFoundException;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import org.apache.airavata.model.user.UserProfile;
import org.apache.airavata.research.service.AiravataService;
import org.apache.airavata.research.service.dto.CreateResourceRequest;
import org.apache.airavata.research.service.dto.ModifyResourceRequest;
import org.apache.airavata.research.service.dto.ResourceResponse;
import org.apache.airavata.research.service.enums.ResourceTypeEnum;
+import org.apache.airavata.research.service.enums.StateEnum;
import org.apache.airavata.research.service.enums.StatusEnum;
import org.apache.airavata.research.service.model.UserContext;
import org.apache.airavata.research.service.model.entity.RepositoryResource;
@@ -71,10 +74,9 @@ public class ResourceHandler {
try {
UserProfile fetchedUser =
airavataService.getUserProfile(authorId);
userSet.add(fetchedUser.getUserId());
-
} catch (Exception e) {
LOGGER.error("Error while fetching user profile with the
userId: {}", authorId, e);
- throw new RuntimeException("Error while fetching user profile
with the userId: " + authorId, e);
+ throw new EntityNotFoundException("Error while fetching user
profile with the userId: " + authorId, e);
}
}
@@ -89,6 +91,7 @@ public class ResourceHandler {
}
resource.setAuthors(userSet);
resource.setTags(tags);
+ resource.setState(StateEnum.ACTIVE);
}
public ResourceResponse createResource(Resource resource, ResourceTypeEnum
type) {
@@ -118,7 +121,9 @@ public class ResourceHandler {
resource.setName(createResourceRequest.getName());
resource.setDescription(createResourceRequest.getDescription());
- resource.setAuthors(createResourceRequest.getAuthors());
+ resource.setAuthors(createResourceRequest.getAuthors().stream()
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet()));
Set<org.apache.airavata.research.service.model.entity.Tag> tagsSet =
new HashSet<>();
for (String tag : createResourceRequest.getTags()) {
org.apache.airavata.research.service.model.entity.Tag t =
@@ -142,10 +147,13 @@ public class ResourceHandler {
public Resource modifyResource(ModifyResourceRequest resourceRequest) {
Optional<Resource> resourceOp =
resourceRepository.findById(resourceRequest.getId());
if (resourceOp.isEmpty()) {
- throw new RuntimeException("Resource not found");
+ throw new EntityNotFoundException("Resource not found");
}
Resource resource = resourceOp.get();
+ if (StateEnum.DELETED.equals(resource.getState())) {
+ throw new RuntimeException(String.format("Cannot modify deleted
resource: %s", resource.getId()));
+ }
// ensure that the user making the request is one of the current
authors
boolean found = false;
@@ -168,15 +176,38 @@ public class ResourceHandler {
public Resource getResourceById(String id) {
// Your logic to fetch the resource by ID
- Optional<Resource> opResource = resourceRepository.findById(id);
+ Optional<Resource> opResource =
resourceRepository.findByIdAndState(id, StateEnum.ACTIVE);
if (opResource.isEmpty()) {
- throw new RuntimeException("Resource not found: " + id);
+ throw new EntityNotFoundException("Resource not found: " + id);
}
return opResource.get();
}
+ public boolean deleteResourceById(String id) {
+ Optional<Resource> opResource =
resourceRepository.findByIdAndState(id, StateEnum.ACTIVE);
+
+ if (opResource.isEmpty()) {
+ throw new EntityNotFoundException("Resource not found: " + id);
+ }
+
+ Resource resource = opResource.get();
+
+ String userEmail = UserContext.userId();
+ if (!resource.getAuthors().contains(userEmail.toLowerCase())) {
+ String errorMsg = String.format(
+ "User %s not authorized to delete resource: %s (%s), type:
%s",
+ userEmail, resource.getName(), id,
resource.getType().toString());
+ LOGGER.error(errorMsg);
+ throw new RuntimeException(errorMsg);
+ }
+
+ resource.setState(StateEnum.DELETED);
+ resourceRepository.delete(resource);
+ return true;
+ }
+
public Page<Resource> getAllResources(
int pageNumber, int pageSize, List<Class<? extends Resource>>
typeList, String[] tag, String nameSearch) {
Pageable pageable = PageRequest.of(pageNumber, pageSize);
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Project.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Project.java
index 9614190b96..ca3d67ee87 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Project.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Project.java
@@ -22,6 +22,8 @@ package org.apache.airavata.research.service.model.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@@ -32,6 +34,7 @@ import jakarta.persistence.ManyToOne;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
+import org.apache.airavata.research.service.enums.StateEnum;
import org.hibernate.annotations.UuidGenerator;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
@@ -72,6 +75,10 @@ public class Project {
@LastModifiedDate
private Instant updatedAt;
+ @Column(nullable = false)
+ @Enumerated(EnumType.STRING)
+ private StateEnum state;
+
public String getId() {
return id;
}
@@ -131,4 +138,12 @@ public class Project {
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
+
+ public StateEnum getState() {
+ return state;
+ }
+
+ public void setState(StateEnum state) {
+ this.state = state;
+ }
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java
index d4a88ee6f9..97814f3163 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/entity/Resource.java
@@ -41,6 +41,7 @@ import java.util.HashSet;
import java.util.Set;
import org.apache.airavata.research.service.enums.PrivacyEnum;
import org.apache.airavata.research.service.enums.ResourceTypeEnum;
+import org.apache.airavata.research.service.enums.StateEnum;
import org.apache.airavata.research.service.enums.StatusEnum;
import org.hibernate.annotations.UuidGenerator;
import org.springframework.data.annotation.CreatedDate;
@@ -84,6 +85,10 @@ public abstract class Resource {
@Enumerated(EnumType.STRING)
private StatusEnum status;
+ @Column(nullable = false)
+ @Enumerated(EnumType.STRING)
+ private StateEnum state;
+
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private PrivacyEnum privacy;
@@ -146,19 +151,27 @@ public abstract class Resource {
this.tags = tags;
}
- public org.apache.airavata.research.service.enums.StatusEnum getStatus() {
+ public StatusEnum getStatus() {
return status;
}
- public void
setStatus(org.apache.airavata.research.service.enums.StatusEnum status) {
+ public void setStatus(StatusEnum status) {
this.status = status;
}
- public org.apache.airavata.research.service.enums.PrivacyEnum getPrivacy()
{
+ public StateEnum getState() {
+ return state;
+ }
+
+ public void setState(StateEnum state) {
+ this.state = state;
+ }
+
+ public PrivacyEnum getPrivacy() {
return privacy;
}
- public void
setPrivacy(org.apache.airavata.research.service.enums.PrivacyEnum privacy) {
+ public void setPrivacy(PrivacyEnum privacy) {
this.privacy = privacy;
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ProjectRepository.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ProjectRepository.java
index adc9916013..0b6ff35a0b 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ProjectRepository.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ProjectRepository.java
@@ -20,7 +20,9 @@
package org.apache.airavata.research.service.model.repo;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
+import org.apache.airavata.research.service.enums.StateEnum;
import org.apache.airavata.research.service.model.entity.DatasetResource;
import org.apache.airavata.research.service.model.entity.Project;
import org.apache.airavata.research.service.model.entity.RepositoryResource;
@@ -39,4 +41,17 @@ public interface ProjectRepository extends
JpaRepository<Project, String> {
List<Project> findAllByOwnerId(String ownerId);
List<Project> findAllByOwnerIdOrderByCreatedAtDesc(String ownerId);
+
+ List<Project> findALlByState(StateEnum state);
+
+ List<Project> findProjectsByRepositoryResourceAndState(RepositoryResource
repositoryResource, StateEnum state);
+
+ List<Project> findAllByOwnerIdAndStateOrderByCreatedAtDesc(String ownerId,
StateEnum state);
+
+ List<Project> findProjectsByDatasetResourcesContainingAndState(
+ Set<DatasetResource> datasetResources, StateEnum state);
+
+ StateEnum State(StateEnum state);
+
+ Optional<Project> findByIdAndState(String id, StateEnum state);
}
diff --git
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
index 7036d0e814..e53058ab10 100644
---
a/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
+++
b/modules/research-framework/research-service/src/main/java/org/apache/airavata/research/service/model/repo/ResourceRepository.java
@@ -20,6 +20,8 @@
package org.apache.airavata.research.service.model.repo;
import java.util.List;
+import java.util.Optional;
+import org.apache.airavata.research.service.enums.StateEnum;
import org.apache.airavata.research.service.model.entity.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -35,7 +37,7 @@ public interface ResourceRepository extends
JpaRepository<Resource, String> {
"""
SELECT r
FROM #{#entityName} r
- WHERE TYPE(r) IN :types AND r.name LIKE CONCAT('%',
:nameSearch, '%')
+ WHERE TYPE(r) IN :types AND r.name LIKE CONCAT('%',
:nameSearch, '%') AND r.state = 'ACTIVE'
ORDER BY r.name
""")
Page<Resource> findAllByTypes(
@@ -51,6 +53,7 @@ public interface ResourceRepository extends
JpaRepository<Resource, String> {
WHERE r.class IN :typeList
AND t.value IN :tags
AND LOWER(r.name) LIKE LOWER(CONCAT('%', :nameSearch,
'%'))
+ AND r.state = 'ACTIVE'
GROUP BY r
HAVING COUNT(DISTINCT t.value) = :tagCount
ORDER BY r.name
@@ -66,9 +69,11 @@ public interface ResourceRepository extends
JpaRepository<Resource, String> {
"""
SELECT r
FROM Resource r
- WHERE TYPE(r) = :type
+ WHERE TYPE(r) = :type AND r.state = 'ACTIVE'
AND LOWER(r.name) LIKE LOWER(CONCAT('%', :name, '%'))
""")
List<Resource> findByTypeAndNameContainingIgnoreCase(
@Param("type") Class<? extends Resource> type, @Param("name")
String name);
+
+ Optional<Resource> findByIdAndState(String id, StateEnum state);
}
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 ceb9916601..e59360f7ce 100644
---
a/modules/research-framework/research-service/src/main/resources/application.yml
+++
b/modules/research-framework/research-service/src/main/resources/application.yml
@@ -55,7 +55,7 @@ spring:
leak-detection-threshold: 20000
jpa:
hibernate:
- ddl-auto: none
+ ddl-auto: update
open-in-view: false
springdoc: