diff --git a/client/app/settings/datasets/AddNewCard.tsx b/client/app/settings/datasets/AddNewCard.tsx index a01c0c98a..207d05f2e 100644 --- a/client/app/settings/datasets/AddNewCard.tsx +++ b/client/app/settings/datasets/AddNewCard.tsx @@ -4,7 +4,11 @@ import Card from "@/components/card"; import { useAppStore } from "@/store"; import React from "react"; -const AddNewCard = () => { +interface IProps { + text: string; +} + +const AddNewCard = ({ text }: IProps) => { const darkMode = useAppStore((state) => state.darkMode); return ( {

- New dataset + {text}

diff --git a/client/app/settings/datasets/page.tsx b/client/app/settings/datasets/page.tsx index 32df70610..4d041ab6e 100644 --- a/client/app/settings/datasets/page.tsx +++ b/client/app/settings/datasets/page.tsx @@ -47,7 +47,7 @@ export default async function Datasets() { ))} - + diff --git a/client/app/settings/workspaces/[id]/WorkspaceCard.tsx b/client/app/settings/workspaces/[id]/WorkspaceCard.tsx new file mode 100644 index 000000000..c37328d7b --- /dev/null +++ b/client/app/settings/workspaces/[id]/WorkspaceCard.tsx @@ -0,0 +1,79 @@ +"use client"; +import Card from "@/components/card"; +import ConfirmationDialog from "@/components/ConfirmationDialog"; +import { Button } from "@/components/ui/button"; +import { useDeleteWorkspace } from "@/hooks/useSpaces"; +import { revalidateWorkspaces } from "@/lib/actions"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; +import { toast } from "react-toastify"; + +interface IProps { + data: any; +} + +const WorkspaceCard = ({ data }: IProps) => { + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const { isPending, mutateAsync: deleteSpace } = useDeleteWorkspace(); + const router = useRouter(); + + const handleDeleteSpace = () => { + deleteSpace( + { id: data.id }, + { + onSuccess(response) { + toast.success(response?.data?.message); + setIsDeleteModalOpen(false); + revalidateWorkspaces(); + router.push("/settings/workspaces"); + }, + onError(error) { + toast.error(error?.message); + }, + } + ); + }; + return ( + <> + +
+
+

{data?.name}

+

+ + {`This is the workspace dedicated to the ${data?.name} team.`} + +

+
+
+ + + + +
{ + setIsDeleteModalOpen(true); + }} + className={`px-8 py-1 rounded-[10px] flex flex-wrap items-center justify-center cursor-pointer dark:bg-[#D30000] neon-on-hovers`} + > + Delete +
+
+
+
+ {isDeleteModalOpen && ( + { + setIsDeleteModalOpen(false); + }} + onSubmit={handleDeleteSpace} + isLoading={isPending} + /> + )} + + ); +}; + +export default WorkspaceCard; diff --git a/client/app/settings/workspaces/[id]/page.tsx b/client/app/settings/workspaces/[id]/page.tsx new file mode 100644 index 000000000..83f12d181 --- /dev/null +++ b/client/app/settings/workspaces/[id]/page.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import Card from "components/card"; + +import { GetWorkspaceDetails } from "@/services/spaces"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import AddNewCard from "../../datasets/AddNewCard"; +import { FaFileCsv } from "react-icons/fa"; +import WorkspaceCard from "./WorkspaceCard"; + +interface PageProps { + params: { + id: string; + }; +} + +export default async function WorkSpacesDetails({ params }: PageProps) { + const data = await GetWorkspaceDetails(params.id); + + return ( +
+

+ Workspaces + {data?.name && ` › ${data?.name}`} +

+ +
+
+ + +
+
+

People

+
+ {data?.name} +
+
+
+
+
+ +

+ Datasets +

+
+ {data?.dataset_spaces?.map((df) => ( + +
+
+
+ +
+
+

+ {df?.dataset?.name} +

+
+
+
+
+ + + +
+
+ ))} + + + +
+
+
+ ); +} diff --git a/client/app/settings/workspaces/addspaces/AddSpaceCard.tsx b/client/app/settings/workspaces/addspaces/AddSpaceCard.tsx new file mode 100644 index 000000000..3af445b13 --- /dev/null +++ b/client/app/settings/workspaces/addspaces/AddSpaceCard.tsx @@ -0,0 +1,127 @@ +"use client"; +import React, { useState } from "react"; +import { Loader } from "components/loader/Loader"; +import Multiselect from "multiselect-react-dropdown"; +import { useRouter } from "next/navigation"; +import { toast } from "react-toastify"; +import { AddWorkspace } from "@/services/spaces"; +import { revalidateWorkspaces } from "@/lib/actions"; + +interface IProps { + datasets: any; +} + +interface DataFrame { + id: number; + name: string; +} +const AddSpaceCard = ({ datasets }: IProps) => { + const [name, setName] = useState(""); + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [nameError, setNameError] = useState(""); + const [dataFramesError, setDataFramesError] = useState(""); + const dataFrameNames = []; + const dataFrameIds: number[] = []; + const router = useRouter(); + + items.forEach((data) => { + return dataFrameIds?.push(data.id); + }); + + datasets?.map((data) => { + return dataFrameNames.push({ id: data?.id, name: data?.name }); + }); + + const handleSelect = (selectedList) => { + setItems(selectedList); + }; + + const handleSelectRemove = (selectedList) => { + setItems(selectedList); + }; + + const handleSubmit = async () => { + setNameError(""); + let isValid = true; + if (name?.trim() === "") { + setNameError("Name field is required"); + isValid = false; + } + + if (items?.length === 0) { + setDataFramesError("Tables is required"); + isValid = false; + } + + if (!isValid) { + return; + } + setIsLoading(true); + const payLoad = { + name: name, + datasets: [...dataFrameIds], + }; + await AddWorkspace(payLoad) + .then((response) => { + toast.success(response?.data?.message); + setName(""); + revalidateWorkspaces(); + router.push("/settings/workspaces"); + }) + .catch((error) => { + toast.error( + error?.response?.data?.message + ? error?.response?.data?.message + : error?.message + ); + }) + .finally(() => setIsLoading(false)); + }; + + return ( +
+
+ + setName(e.target.value)} + className={`w-full p-2 border rounded dark:!bg-darkMain `} + /> + {nameError && name?.length < 1 && ( +
+ {nameError} +
+ )} +
+
+ + + {dataFramesError && items?.length < 1 && ( +
+ {dataFramesError} +
+ )} +
+
+ +
+
+ ); +}; + +export default AddSpaceCard; diff --git a/client/app/settings/workspaces/addspaces/loading.tsx b/client/app/settings/workspaces/addspaces/loading.tsx new file mode 100644 index 000000000..8a53eb483 --- /dev/null +++ b/client/app/settings/workspaces/addspaces/loading.tsx @@ -0,0 +1,12 @@ +import { Loader } from "@/components/loader/Loader"; +import React from "react"; + +const Loading = () => { + return ( +
+ +
+ ); +}; + +export default Loading; diff --git a/client/app/settings/workspaces/addspaces/page.tsx b/client/app/settings/workspaces/addspaces/page.tsx new file mode 100644 index 000000000..2af49408d --- /dev/null +++ b/client/app/settings/workspaces/addspaces/page.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import Link from "next/link"; +import { GetAllDataSets } from "@/services/datasets"; +import AddSpaceCard from "./AddSpaceCard"; +import { Button } from "@/components/ui/button"; + +export default async function AddSpaces() { + const data = await GetAllDataSets(); + + return ( +
+

+ Workspaces + › New +

+ + {data?.datasets?.length === 0 ? ( +
+

+ No datasets available, please add one +

+ + + +
+ ) : ( + + )} +
+ ); +} diff --git a/client/app/settings/workspaces/editspaces/EditSpaceCard.tsx b/client/app/settings/workspaces/editspaces/EditSpaceCard.tsx new file mode 100644 index 000000000..d14a4facd --- /dev/null +++ b/client/app/settings/workspaces/editspaces/EditSpaceCard.tsx @@ -0,0 +1,128 @@ +"use client"; +import React, { useState } from "react"; +import { Loader } from "components/loader/Loader"; +import Multiselect from "multiselect-react-dropdown"; +import { useRouter } from "next/navigation"; +import { toast } from "react-toastify"; +import { UpdateWorkspace } from "@/services/spaces"; +import { revalidateWorkspaces } from "@/lib/actions"; + +interface IProps { + datasets: any; + workspaceId?: string; +} + +interface DataFrame { + id: number; + name: string; +} +const EditSpaceCard = ({ datasets, workspaceId }: IProps) => { + const [name, setName] = useState(""); + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [nameError, setNameError] = useState(""); + const [dataFramesError, setDataFramesError] = useState(""); + const dataFrameNames = []; + const dataFrameIds: number[] = []; + const router = useRouter(); + + items.forEach((data) => { + return dataFrameIds?.push(data.id); + }); + + datasets?.map((data) => { + return dataFrameNames.push({ id: data?.id, name: data?.name }); + }); + + const handleSelect = (selectedList) => { + setItems(selectedList); + }; + + const handleSelectRemove = (selectedList) => { + setItems(selectedList); + }; + + const handleSubmit = async () => { + setNameError(""); + let isValid = true; + if (name?.trim() === "") { + setNameError("Name field is required"); + isValid = false; + } + + if (items?.length === 0) { + setDataFramesError("Tables is required"); + isValid = false; + } + + if (!isValid) { + return; + } + setIsLoading(true); + const payLoad = { + name: name, + datasets: [...dataFrameIds], + }; + await UpdateWorkspace(workspaceId, payLoad) + .then((response) => { + toast.success(response?.data?.message); + setName(""); + revalidateWorkspaces(); + router.push("/settings/workspaces"); + }) + .catch((error) => { + toast.error( + error?.response?.data?.message + ? error?.response?.data?.message + : error?.message + ); + }) + .finally(() => setIsLoading(false)); + }; + + return ( +
+
+ + setName(e.target.value)} + className={`w-full p-2 border rounded dark:!bg-darkMain `} + /> + {nameError && name.length < 1 && ( +
+ {nameError} +
+ )} +
+
+ + + {dataFramesError && items.length < 1 && ( +
+ {dataFramesError} +
+ )} +
+
+ +
+
+ ); +}; + +export default EditSpaceCard; diff --git a/client/app/settings/workspaces/editspaces/loading.tsx b/client/app/settings/workspaces/editspaces/loading.tsx new file mode 100644 index 000000000..8a53eb483 --- /dev/null +++ b/client/app/settings/workspaces/editspaces/loading.tsx @@ -0,0 +1,12 @@ +import { Loader } from "@/components/loader/Loader"; +import React from "react"; + +const Loading = () => { + return ( +
+ +
+ ); +}; + +export default Loading; diff --git a/client/app/settings/workspaces/editspaces/page.tsx b/client/app/settings/workspaces/editspaces/page.tsx new file mode 100644 index 000000000..0254d4310 --- /dev/null +++ b/client/app/settings/workspaces/editspaces/page.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import Link from "next/link"; +import { GetAllDataSets } from "@/services/datasets"; +import EditSpaceCard from "./EditSpaceCard"; +import { GetWorkspaceDetails } from "@/services/spaces"; + +export default async function EditWorkSpaces({ searchParams }) { + const data = await GetAllDataSets(); + const workspaceDetails = await GetWorkspaceDetails(searchParams.id); + + return ( +
+

+ Workspaces + + + + {workspaceDetails?.name && ` › ${workspaceDetails?.name}`} + + {" "} + › Edit + +

+ +
+ ); +} diff --git a/client/app/settings/workspaces/loading.tsx b/client/app/settings/workspaces/loading.tsx new file mode 100644 index 000000000..8a53eb483 --- /dev/null +++ b/client/app/settings/workspaces/loading.tsx @@ -0,0 +1,12 @@ +import { Loader } from "@/components/loader/Loader"; +import React from "react"; + +const Loading = () => { + return ( +
+ +
+ ); +}; + +export default Loading; diff --git a/client/app/settings/workspaces/page.tsx b/client/app/settings/workspaces/page.tsx new file mode 100644 index 000000000..2a8b1f4f2 --- /dev/null +++ b/client/app/settings/workspaces/page.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import Card from "components/card"; +import Link from "next/link"; +import AddNewCard from "../datasets/AddNewCard"; +import AppTooltip from "@/components/AppTooltip"; +import { Button } from "@/components/ui/button"; +import { GetAllWorkspaces } from "@/services/spaces"; + +export default async function WorkSpaces() { + const data = await GetAllWorkspaces(); + + return ( +
+

Workspaces

+ +
+ {data?.map((item, index) => ( + +
+
+ +

+ {item.name} +

+
+
+ +
+ + + +
+
+
+ ))} + + + +
+
+ ); +} diff --git a/client/app/settings/workspaces/space-interface.ts b/client/app/settings/workspaces/space-interface.ts new file mode 100644 index 000000000..f66ab0451 --- /dev/null +++ b/client/app/settings/workspaces/space-interface.ts @@ -0,0 +1,48 @@ +export interface SpacesData { + length: number; + dataframes: number[]; + dataframes_count: number; + id: string; + name: string; +} +export interface SpacesContextData { + data?: { + user: { + firstName: string; + lastName: string; + }; + stateChangevalue: boolean; + isAdmin: string; + adminStatus: string; + spaces?: { + spaceLoading: boolean; + spacesData?: SpacesData[]; + }; + }; + setGlobalData?: (data: unknown) => void; +} + +export interface SpaceDataDetail { + dataframes_count: React.ReactNode; + length: number; + name?: string; + id?: number | string; + dataframes?: { + dataframe: { + name?: string; + description?: string; + dataframes_count: number; + id: number; + headers?: string[]; + rows?: (string | number)[][] | { [key: string]: React.ReactNode }[]; + }; + }[]; +} +export interface LogsContextData { + data?: { + stateChangevalue: boolean; + isAdmin: string; + adminStatus: string; + }; + setGlobalData?: (data: unknown) => void; +} diff --git a/client/components/SettingsLayout/LeftBar.tsx b/client/components/SettingsLayout/LeftBar.tsx index 99bac4c07..369b4e8d1 100644 --- a/client/components/SettingsLayout/LeftBar.tsx +++ b/client/components/SettingsLayout/LeftBar.tsx @@ -19,6 +19,11 @@ const routes = [ url: "/settings/logs", isAdmin: true, }, + { + name: "Workspaces", + url: "/settings/workspaces", + isAdmin: true, + }, ]; const SettingsLeftBar = ({ isMobileView = false }: IProps) => { diff --git a/client/hooks/useSpaces.tsx b/client/hooks/useSpaces.tsx index 42bf47db2..b10b37eff 100644 --- a/client/hooks/useSpaces.tsx +++ b/client/hooks/useSpaces.tsx @@ -2,18 +2,10 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AddNewUserSpace, DeleteSpaceUser, - GetAllSpacesList, + DeleteWorkspace, GetSpaceUser, } from "@/services/spaces"; -export const useGetAllSpaces = () => { - const { data, isLoading, error, isError } = useQuery({ - queryKey: ["useGetAllSpaces"], - queryFn: GetAllSpacesList, - }); - return { data, isLoading, error, isError }; -}; - export const useGetSpaceUsers = (spaceId: string) => { const { data, isLoading, error, isError } = useQuery({ queryKey: ["useGetSpaceUsers", spaceId], @@ -45,3 +37,10 @@ export const useAddSpaceUsers = () => { }); return { data, isPending, error, isError, mutateAsync }; }; +export const useDeleteWorkspace = () => { + const { data, isPending, error, isError, mutateAsync } = useMutation({ + mutationFn: (params: any) => DeleteWorkspace(params.id), + onSuccess: () => {}, + }); + return { data, isPending, error, isError, mutateAsync }; +}; diff --git a/client/lib/actions.ts b/client/lib/actions.ts index bad45b97d..75e547fd9 100644 --- a/client/lib/actions.ts +++ b/client/lib/actions.ts @@ -5,4 +5,9 @@ import { revalidateTag } from 'next/cache' export async function revalidateDatasets() { revalidateTag('GetDatasetDetails') revalidateTag('GetAllDataSets') +} + +export async function revalidateWorkspaces() { + revalidateTag('GetWorkspaceDetails') + revalidateTag('GetAllWorkspaces') } \ No newline at end of file diff --git a/client/services/spaces.ts b/client/services/spaces.ts index 1ef76fc21..b47a49f74 100644 --- a/client/services/spaces.ts +++ b/client/services/spaces.ts @@ -1,16 +1,64 @@ -import { DeleteRequestWithBody, GetRequest, PostRequest } from "@/utils/apiUtils"; +import { DeleteRequest, DeleteRequestWithBody, GetRequest, PostRequest, PutRequest } from "@/utils/apiUtils"; import { BASE_API_URL } from "utils/constants"; -const spaceApiUrl = `${BASE_API_URL}/api/spaces`; +const spaceApiUrl = `${BASE_API_URL}/v1/workspace`; type userSpace = { new_member_email: string; }; -export const GetAllSpacesList = async () => { - const apiUrl = `${spaceApiUrl}/list`; - const data = await GetRequest(apiUrl); - return data; + +export const GetAllWorkspaces = async () => { + try { + const response = await fetch(`${spaceApiUrl}/list`, { next: { tags: ['GetAllWorkspaces'] } }); + return await response.json(); + } catch (error) { + console.error('Get request failed', error); + throw error; + } +}; + +export const GetWorkspaceDetails = async (id: string) => { + try { + const response = await fetch(`${spaceApiUrl}/${id}/details`, { next: { tags: ['GetWorkspaceDetails'] } }); + return await response.json(); + } catch (error) { + console.error('Get request failed', error); + throw error; + } +}; + +export const AddWorkspace = async (body) => { + const apiUrl = `${spaceApiUrl}/add`; + try { + const data = await PostRequest(apiUrl, body); + return data; + } catch (error) { + console.error("add request failed", error); + throw error; + } +}; + +export const UpdateWorkspace = async (id: string, body) => { + const apiUrl = `${spaceApiUrl}/${id}/edit`; + try { + const data = await PutRequest(apiUrl, body); + return data; + } catch (error) { + console.error("add request failed", error); + throw error; + } +}; + +export const DeleteWorkspace = async (id: string) => { + const apiUrl = `${spaceApiUrl}/${id}`; + try { + const data = await DeleteRequest(apiUrl); + return data; + } catch (error) { + console.error("Delete request failed", error); + throw error; + } }; export const GetSpaceUser = async (id: string) => { diff --git a/server/api/v1/workspace/workspace.py b/server/api/v1/workspace/workspace.py index b7b82ab86..381b864a7 100644 --- a/server/api/v1/workspace/workspace.py +++ b/server/api/v1/workspace/workspace.py @@ -7,10 +7,21 @@ from core.fastapi.dependencies.current_user import get_current_user from app.schemas.responses.users import WorkspaceUsersResponse from app.schemas.responses.datasets import WorkspaceDatasetsResponseModel +from app.schemas.requests.workspace import WorkspaceCreateRequestModel +from typing import List workspace_router = APIRouter() +@workspace_router.get("/list") +async def get_user_workspaces( + workspace_controller: WorkspaceController = Depends( + Factory().get_space_controller + ), + user: UserInfo = Depends(get_current_user)): + return await workspace_controller.get_user_workspaces(user) + + @workspace_router.get("/{workspace_id}/users", response_model=WorkspaceUsersResponse) async def get_workspace_users( workspace_id: UUID = Path(..., description="ID of the workspace"), @@ -25,4 +36,38 @@ async def get_workspace_datasets( workspace_id: UUID = Path(..., description="ID of the workspace"), workspace_controller: WorkspaceController = Depends(Factory().get_space_controller) ): - return await workspace_controller.get_workspace_datasets(workspace_id) \ No newline at end of file + return await workspace_controller.get_workspace_datasets(workspace_id) + + +@workspace_router.get("/{workspace_id}/details") +async def get_workspace_details( + workspace_id: UUID = Path(..., description="ID of the workspace"), + workspace_controller: WorkspaceController = Depends(Factory().get_space_controller) + ): + return await workspace_controller.get_workspace_datails(workspace_id) + + +@workspace_router.delete("/{workspace_id}") +async def delete_workspace( + workspace_id: UUID = Path(..., description="ID of the workspace"), + workspace_controller: WorkspaceController = Depends(Factory().get_space_controller) + ): + return await workspace_controller.delete_workspace(workspace_id) + + +@workspace_router.post("/add") +async def add_workspace( + workspace: WorkspaceCreateRequestModel, + workspace_controller: WorkspaceController = Depends(Factory().get_space_controller), + user: UserInfo = Depends(get_current_user) + ): + return await workspace_controller.add_workspace(workspace, user) + + +@workspace_router.put("/{workspace_id}/edit") +async def edit_workspace( + workspace: WorkspaceCreateRequestModel, + workspace_id: UUID = Path(..., description="ID of the workspace"), + workspace_controller: WorkspaceController = Depends(Factory().get_space_controller), + ): + return await workspace_controller.edit_workspace(workspace_id, workspace) \ No newline at end of file diff --git a/server/app/controllers/workspace.py b/server/app/controllers/workspace.py index 57b6ee235..177f6d4e8 100644 --- a/server/app/controllers/workspace.py +++ b/server/app/controllers/workspace.py @@ -69,4 +69,36 @@ async def get_workspace_datasets(self, workspace_id) -> WorkspaceDatasetsRespons status_code=status.HTTP_404_NOT_FOUND, detail="No dataset found. Please restart the server and try again" ) - return WorkspaceDatasetsResponseModel(datasets=datasets) \ No newline at end of file + return WorkspaceDatasetsResponseModel(datasets=datasets) + + + async def get_user_workspaces(self, user): + result = await self.space_repository.get_user_workspaces(user) + + return result + + async def get_workspace_datails(self, workspace_id) -> WorkspaceDatasetsResponseModel: + await self.get_workspace_by_id(workspace_id) + workspace = await self.space_repository.get_workspace_datails(workspace_id) + if not workspace: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No workspace found. Please restart the server and try again" + ) + return workspace + + + async def delete_workspace(self, workspace_id): + await self.space_repository.delete_workspace(workspace_id) + return {"message": "Workspace deleted successfully"} + + + + async def add_workspace(self, workspace, user): + await self.space_repository.create_workspace(workspace, user) + return {"message": "Workspace successfully Added"} + + + async def edit_workspace(self, workspace_id, workspace): + await self.space_repository.edit_workspace(workspace_id, workspace) + return {"message": "Workspace successfully Updated"} \ No newline at end of file diff --git a/server/app/repositories/workspace.py b/server/app/repositories/workspace.py index 1923f4a5a..8d9931d1e 100644 --- a/server/app/repositories/workspace.py +++ b/server/app/repositories/workspace.py @@ -1,6 +1,6 @@ +from fastapi import HTTPException from typing import List - -from sqlalchemy import delete, select +from sqlalchemy import delete, select, update from sqlalchemy.orm import joinedload from app.models import Connector, Dataset, DatasetSpace, Workspace, UserSpace, User @@ -8,7 +8,7 @@ from core.config import config from core.repository import BaseRepository from app.schemas.responses.users import WorkspaceUserResponse - +from core.database.transactional import Propagation, Transactional class WorkspaceRepository(BaseRepository[Workspace]): """ @@ -120,4 +120,115 @@ async def delete_datasetspace(self, dataset_id: str, workspace_id: str): # Delete the Dataset entry await self.session.execute( delete(Dataset).where(Dataset.id == dataset_id) - ) \ No newline at end of file + ) + + async def get_user_workspaces(self, user): + space_result = await self.session.execute( + select(Workspace).filter(Workspace.user_id == user.id) + ) + user_spaces = space_result.scalars().all() + + + return user_spaces + + + async def get_workspace_datails(self, workspace_id: str): + result = await self.session.execute( + select(Workspace) + .options( + joinedload(Workspace.dataset_spaces) + .joinedload(DatasetSpace.dataset) + .joinedload(Dataset.connector) + ) + .where(Workspace.id == workspace_id) + ) + workspace = result.scalars().first() + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + return workspace + + + + async def delete_workspace(self, workspace_id: str): + async with self.session.begin(): + result = await self.session.execute( + select(Workspace) + .options( + joinedload(Workspace.dataset_spaces), + joinedload(Workspace.user_spaces) + ) + .where(Workspace.id == workspace_id) + ) + workspace = result.scalars().first() + + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + + for dataset_space in workspace.dataset_spaces: + await self.session.delete(dataset_space) + + for user_space in workspace.user_spaces: + await self.session.delete(user_space) + + await self.session.delete(workspace) + await self.session.commit() + + + + @Transactional(propagation=Propagation.REQUIRED_NEW) + async def create_workspace(self, workspace_data, user): + new_workspace = Workspace( + name=workspace_data.name, + user_id=user.id, + organization_id=user.organizations[0].id, + ) + self.session.add(new_workspace) + await self.session.flush() + + dataset_spaces = [ + DatasetSpace(workspace_id=new_workspace.id, dataset_id=dataset_id) + for dataset_id in workspace_data.datasets + ] + self.session.add_all(dataset_spaces) + + user_space = UserSpace(workspace_id=new_workspace.id, user_id=user.id) + self.session.add(user_space) + + return new_workspace + + + @Transactional(propagation=Propagation.REQUIRED_NEW) + async def edit_workspace(self, workspace_id, workspace_data): + async with self.session.begin(): + result = await self.session.execute( + select(Workspace) + .options( + joinedload(Workspace.dataset_spaces), + ) + .where(Workspace.id == workspace_id) + ) + workspace = result.scalars().first() + + if not workspace: + raise HTTPException(status_code=404, detail="Workspace not found") + + await self.session.execute( + delete(DatasetSpace).where(DatasetSpace.workspace_id == workspace_id) + ) + + update_data = { + Workspace.name: workspace_data.name + } + + await self.session.execute( + update(Workspace) + .where(Workspace.id == workspace_id) + .values(update_data) + ) + dataset_spaces = [ + DatasetSpace(workspace_id=workspace_id, dataset_id=dataset_id) + for dataset_id in workspace_data.datasets + ] + self.session.add_all(dataset_spaces) + await self.session.commit() + return workspace diff --git a/server/app/schemas/requests/workspace.py b/server/app/schemas/requests/workspace.py new file mode 100644 index 000000000..0bb08b647 --- /dev/null +++ b/server/app/schemas/requests/workspace.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel +from typing import List + + +class WorkspaceCreateRequestModel(BaseModel): + name: str + datasets: List[str] \ No newline at end of file