From 3769ffa41fe033549ef7027e5285116ecde5abbb Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:16:16 +0000 Subject: [PATCH] feat(web): Company Brain connections disconnect and Slack reconnect (#1157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add disconnect actions for org and personal GitHub/Linear connections on the Company Brain settings page - Show connected Slack workspace name and an admin-only **Reconnect Slack** button (pairs with API admin gate in supermemoryai/mono#1912) - Fix infinite loading skeleton when `/brain/connections` fails by falling back to empty state with a toast ## Test plan - [ ] Open Settings → Company Brain connections as org admin - [ ] Confirm Slack team name shows when workspace is connected - [ ] Confirm **Reconnect Slack** is visible for admin/owner only - [ ] Connect and disconnect GitHub/Linear for org (admin) and personal scopes - [ ] Simulate failed connections fetch (e.g. offline) and confirm page renders instead of infinite skeleton --- .../settings/company-brain-connections.tsx | 152 +++++++++++++++++- 1 file changed, 144 insertions(+), 8 deletions(-) diff --git a/apps/web/components/settings/company-brain-connections.tsx b/apps/web/components/settings/company-brain-connections.tsx index b9045e8b..7720969e 100644 --- a/apps/web/components/settings/company-brain-connections.tsx +++ b/apps/web/components/settings/company-brain-connections.tsx @@ -3,7 +3,7 @@ import { authClient } from "@lib/auth" import { cn } from "@lib/utils" import { useQuery } from "@tanstack/react-query" -import { Check, Loader2, Lock } from "lucide-react" +import { Loader2, Lock } from "lucide-react" import { useCallback, useEffect, useState } from "react" import { toast } from "sonner" import { dmSans125ClassName } from "@/lib/fonts" @@ -14,6 +14,67 @@ const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" type ConnRow = { toolkit: string; org: boolean; user: boolean } +type SlackStatus = { connected: boolean; teamName: string | null } + +function SecondaryButton({ + children, + href, +}: { + children: React.ReactNode + href: string +}) { + return ( + + {children} + + ) +} + +function SlackMark({ className }: { className?: string }) { + return ( + + ) +} function GithubMark({ className }: { className?: string }) { return ( @@ -73,16 +134,20 @@ function AppCard({ toolkit, connected, canConnect, + canDisconnect, lockedHint, busy, onConnect, + onDisconnect, }: { toolkit: string connected: boolean canConnect: boolean + canDisconnect: boolean lockedHint?: string busy: boolean onConnect: () => void + onDisconnect: () => void }) { const meta = TOOLKITS[toolkit] ?? { label: toolkit, @@ -116,7 +181,13 @@ function AppCard({
- {!connected && + {connected && canDisconnect ? ( + + {busy && } + Disconnect + + ) : ( + !connected && (canConnect ? ( {busy && } @@ -127,7 +198,8 @@ function AppCard({ {lockedHint} - ) : null)} + ) : null) + )}
) @@ -182,6 +254,7 @@ function CardSkeleton() { export default function CompanyBrainConnections() { const isCompanyBrain = useHasCompanyBrain() const [rows, setRows] = useState(null) + const [slackStatus, setSlackStatus] = useState(null) const [busy, setBusy] = useState(null) const roleQuery = useQuery({ @@ -195,11 +268,21 @@ export default function CompanyBrainConnections() { const isAdmin = role === "owner" || role === "admin" const load = useCallback(async () => { - const res = await fetch(`${BACKEND}/brain/connections`, { - credentials: "include", - }) - if (res.ok) - setRows(((await res.json()) as { toolkits: ConnRow[] }).toolkits) + const [connRes, slackRes] = await Promise.all([ + fetch(`${BACKEND}/brain/connections`, { credentials: "include" }), + fetch(`${BACKEND}/brain/slack/status`, { credentials: "include" }), + ]) + if (connRes.ok) { + setRows(((await connRes.json()) as { toolkits: ConnRow[] }).toolkits) + } else { + setRows([]) + toast.error("Couldn't load connections.") + } + if (slackRes.ok) { + setSlackStatus((await slackRes.json()) as SlackStatus) + } else { + setSlackStatus({ connected: false, teamName: null }) + } }, []) useEffect(() => { @@ -235,6 +318,37 @@ export default function CompanyBrainConnections() { } } + const disconnect = async (toolkit: string, scope: "user" | "org") => { + const label = TOOLKITS[toolkit]?.label ?? toolkit + if ( + !window.confirm( + `Disconnect ${label} from ${scope === "org" ? "the shared org account" : "your personal account"}?`, + ) + ) + return + setBusy(`${toolkit}:${scope}`) + try { + const res = await fetch( + `${BACKEND}/brain/connections/${toolkit}?scope=${scope}`, + { method: "DELETE", credentials: "include" }, + ) + if (res.status === 403) { + toast.error("Only admins can disconnect the shared org account.") + return + } + if (!res.ok) { + toast.error("Couldn't disconnect.") + return + } + toast.success(`${label} disconnected.`) + await load() + } catch { + toast.error("Couldn't disconnect.") + } finally { + setBusy(null) + } + } + if (!isCompanyBrain) { return (

+

+ {slackStatus?.connected && slackStatus.teamName ? ( +

+ Slack · {slackStatus.teamName} +

+ ) : null} + {isAdmin ? ( + + + Reconnect Slack + + ) : null} +
connect(row.toolkit, "org")} + onDisconnect={() => disconnect(row.toolkit, "org")} /> )) )} @@ -286,8 +420,10 @@ export default function CompanyBrainConnections() { toolkit={row.toolkit} connected={row.user} canConnect + canDisconnect busy={busy === `${row.toolkit}:user`} onConnect={() => connect(row.toolkit, "user")} + onDisconnect={() => disconnect(row.toolkit, "user")} /> )) )}