Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow user to unsubscribe from a brain #1254

Merged
merged 7 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions backend/models/brains.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from uuid import UUID

from logger import get_logger
from models.databases.supabase.supabase import SupabaseDB
from models.settings import get_supabase_client, get_supabase_db
from pydantic import BaseModel
from supabase.client import Client
from utils.vectors import get_unique_files_from_vector_ids

from models.databases.supabase.supabase import SupabaseDB
from models.settings import get_supabase_client, get_supabase_db

logger = get_logger(__name__)


Expand Down Expand Up @@ -75,11 +76,11 @@ def delete_user_from_brain(self, user_id):
def delete_brain(self, user_id):
results = self.supabase_db.delete_brain_user_by_id(user_id, self.id) # type: ignore

if len(results.data) == 0:
if len(results) == 0:
return {"message": "You are not the owner of this brain."}
else:
self.supabase_db.delete_brain_vector(self.id) # type: ignore
self.supabase_db.delete_brain_user(self.id) # type: ignore
self.supabase_db.delete_brain_users(self.id) # type: ignore
self.supabase_db.delete_brain(self.id) # type: ignore

def create_brain_vector(self, vector_id, file_sha1):
Expand Down
2 changes: 1 addition & 1 deletion backend/models/databases/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def delete_brain_vector(self, brain_id: str):
pass

@abstractmethod
def delete_brain_user(self, brain_id: str):
def delete_brain_users(self, brain_id: str):
pass

@abstractmethod
Expand Down
14 changes: 9 additions & 5 deletions backend/models/databases/supabase/brains.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,18 @@ def get_brain_details(self, brain_id):
)
return response.data

def delete_brain_user_by_id(self, user_id, brain_id):
def delete_brain_user_by_id(
self,
user_id: UUID,
brain_id: UUID,
):
results = (
self.db.table("brains_users")
.select("*")
.match({"brain_id": brain_id, "user_id": user_id, "rights": "Owner"})
.delete()
.match({"brain_id": str(brain_id), "user_id": str(user_id)})
.execute()
)
return results
return results.data

def delete_brain_vector(self, brain_id: str):
results = (
Expand All @@ -146,7 +150,7 @@ def delete_brain_vector(self, brain_id: str):

return results

def delete_brain_user(self, brain_id: str):
def delete_brain_users(self, brain_id: str):
results = (
self.db.table("brains_users")
.delete()
Expand Down
11 changes: 11 additions & 0 deletions backend/repository/brain/delete_brain_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from uuid import UUID

from models.settings import get_supabase_db


def delete_brain_user(user_id: UUID, brain_id: UUID) -> None:
supabase_db = get_supabase_db()
supabase_db.delete_brain_user_by_id(
user_id=user_id,
brain_id=brain_id,
)
35 changes: 35 additions & 0 deletions backend/routes/subscription_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
get_brain_for_user,
update_brain_user_rights,
)
from repository.brain.delete_brain_user import delete_brain_user
from repository.brain_subscription import (
SubscriptionInvitationService,
resend_invitation_email,
Expand Down Expand Up @@ -403,3 +404,37 @@ async def subscribe_to_brain_handler(
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")

return {"message": "You have successfully subscribed to the brain"}


@subscription_router.post(
"/brains/{brain_id}/unsubscribe",
tags=["Subscription"],
)
async def unsubscribe_from_brain_handler(
brain_id: UUID, current_user: UserIdentity = Depends(get_current_user)
):
"""
Unsubscribe from a brain
"""
if not current_user.email:
raise HTTPException(status_code=400, detail="UserIdentity email is not defined")

brain = get_brain_by_id(brain_id)

if brain is None:
raise HTTPException(status_code=404, detail="Brain not found")
if brain.status != "public":
raise HTTPException(
status_code=403,
detail="You cannot subscribe to this brain without invitation",
)
# check if user is already subscribed to brain
user_brain = get_brain_for_user(current_user.id, brain_id)
if user_brain is None:
raise HTTPException(
status_code=403,
detail="You are not subscribed to this brain",
)
delete_brain_user(user_id=current_user.id, brain_id=brain_id)

return {"message": "You have successfully unsubscribed from the brain"}
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,34 @@ import { Content, List, Root } from "@radix-ui/react-tabs";
import { useTranslation } from "react-i18next";

import Button from "@/lib/components/ui/Button";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";

import { BrainTabTrigger, KnowledgeTab, PeopleTab } from "./components";
import ConfirmationDeleteModal from "./components/Modals/ConfirmationDeleteModal";
import { DeleteOrUnsubscribeConfirmationModal } from "./components/Modals/DeleteOrUnsubscribeConfirmationModal";
import { SettingsTab } from "./components/SettingsTab/SettingsTab";
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
import { getBrainPermissions } from "./utils/getBrainPermissions";

export const BrainManagementTabs = (): JSX.Element => {
const { t } = useTranslation(["translation", "config", "delete_brain"]);
const { t } = useTranslation([
"translation",
"config",
"delete_or_unsubscribe_from_brain",
]);
const {
selectedTab,
setSelectedTab,
brainId,
handleDeleteBrain,
isDeleteModalOpen,
setIsDeleteModalOpen,
handleUnsubscribeOrDeleteBrain,
isDeleteOrUnsubscribeModalOpened,
setIsDeleteOrUnsubscribeModalOpened,
hasEditRights,
isOwnedByCurrentUser,
isDeleteOrUnsubscribeRequestPending,
} = useBrainManagementTabs();
const { allBrains } = useBrainContext();

if (brainId === undefined) {
return <div />;
}

const { hasEditRights, isOwnedByCurrentUser } = getBrainPermissions({
brainId,
userAccessibleBrains: allBrains,
});

return (
<Root
className="flex flex-col w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 md:p-10"
Expand Down Expand Up @@ -78,19 +77,33 @@ export const BrainManagementTabs = (): JSX.Element => {
</div>

<div className="flex justify-center mt-4">
<Button
disabled={!isOwnedByCurrentUser}
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteModalOpen(true)}
>
{t("deleteButton", { ns: "delete_brain" })}
</Button>
{isOwnedByCurrentUser ? (
<Button
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
>
{t("deleteButton", { ns: "delete_or_unsubscribe_from_brain" })}
</Button>
) : (
<Button
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
>
{t("unsubscribeButton", {
ns: "delete_or_unsubscribe_from_brain",
})}
</Button>
)}
</div>

<ConfirmationDeleteModal
isOpen={isDeleteModalOpen}
setOpen={setIsDeleteModalOpen}
onDelete={handleDeleteBrain}
<DeleteOrUnsubscribeConfirmationModal
isOpen={isDeleteOrUnsubscribeModalOpened}
setOpen={setIsDeleteOrUnsubscribeModalOpened}
onConfirm={() => void handleUnsubscribeOrDeleteBrain()}
isOwnedByCurrentUser={isOwnedByCurrentUser}
isDeleteOrUnsubscribeRequestPending={
isDeleteOrUnsubscribeRequestPending
}
/>
</Root>
);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useTranslation } from "react-i18next";

import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";

type DeleteOrUnsubscribeConfirmationModalProps = {
isOpen: boolean;
setOpen: (isOpen: boolean) => void;
onConfirm: () => void;
isOwnedByCurrentUser: boolean;
isDeleteOrUnsubscribeRequestPending: boolean;
};

export const DeleteOrUnsubscribeConfirmationModal = ({
isOpen,
setOpen,
onConfirm,
isOwnedByCurrentUser,
isDeleteOrUnsubscribeRequestPending,
}: DeleteOrUnsubscribeConfirmationModalProps): JSX.Element => {
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);

return (
<Modal
desc={
isOwnedByCurrentUser
? t("deleteConfirmQuestion")
: t("unsubscribeConfirmQuestion")
}
isOpen={isOpen}
setOpen={setOpen}
Trigger={<div />}
CloseTrigger={
<Button className="self-end" data-testid="return-button">
{t("returnButton")}
</Button>
}
>
<div>
<div className="flex justify-center mt-6">
<Button
data-testid="delete-brain"
className="px-4 py-2 bg-red-500 text-white rounded-md"
onClick={onConfirm}
isLoading={isDeleteOrUnsubscribeRequestPending}
>
{isOwnedByCurrentUser
? t("deleteConfirmYes")
: t("unsubscribeButton")}
</Button>
</div>
</div>
</Modal>
);
};
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

import ConfirmationDeleteModal from "../ConfirmationDeleteModal";
import { DeleteOrUnsubscribeConfirmationModal } from "../DeleteOrUnsubscribeConfirmationModal";

describe("ConfirmationDeleteModal", () => {
describe("DeleteOrUnsubscribeConfirmationModal", () => {
const isOpen = true;
const setOpen = vi.fn();
const onDelete = vi.fn();
Expand All @@ -14,10 +14,10 @@ describe("ConfirmationDeleteModal", () => {

it("should render delete modal", () => {
const { getByTestId } = render(
<ConfirmationDeleteModal
<DeleteOrUnsubscribeConfirmationModal
isOpen={isOpen}
setOpen={setOpen}
onDelete={onDelete}
onConfirm={onDelete}
/>
);
expect(getByTestId("modal-description")).toBeDefined();
Expand All @@ -27,10 +27,10 @@ describe("ConfirmationDeleteModal", () => {

it("should call onDelete when delete-brain is clicked", () => {
const { getByTestId } = render(
<ConfirmationDeleteModal
<DeleteOrUnsubscribeConfirmationModal
isOpen={isOpen}
setOpen={setOpen}
onDelete={onDelete}
onConfirm={onDelete}
/>
);

Expand Down
Loading