From 8a9e3ff3b074f160698ffb55a16780110b6d7e4f Mon Sep 17 00:00:00 2001 From: MaheshtheDev <38828053+MaheshtheDev@users.noreply.github.com> Date: Wed, 10 Jun 2026 05:00:59 +0000 Subject: [PATCH] feat(integrations): active connections rail + recently added (#1075) > **_Demo Video_**: https://www.tella.tv/video/integrations-page-layout-with-active-connections-1-g9p4 - Right rail listing active plugins/connectors with status, counts, and expandable detail - Recently added card: latest docs tagged by source, opens in the doc viewer - Loading skeletons, empty states, header fetch spinner; fix guest-view flash on reload ENG-784 --- apps/web/app/(app)/page.tsx | 10 +- apps/web/components/integrations-view.tsx | 1029 +++++++++++++++++++-- 2 files changed, 958 insertions(+), 81 deletions(-) diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index cd41476a6..98febe44f 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -108,7 +108,7 @@ function ViewErrorFallback() { export default function NewPage() { const isMobile = useIsMobile() - const { user, session } = useAuth() + const { user, session, isSessionPending } = useAuth() const { selectedProject, selectedProjects, setSelectedProject } = useProject() const selectedProjectTag = selectedProjects[0] @@ -582,7 +582,8 @@ export default function NewPage() { viewMode === "dashboard" || (viewMode === "graph" && isMobile) const isGraphMode = viewMode === "graph" const showBottomNav = isMobile && !!session && !isChatView - const isPublicIntegrations = !session && viewMode === "integrations" + const isPublicIntegrations = + !session && !isSessionPending && viewMode === "integrations" return ( @@ -663,7 +664,10 @@ export default function NewPage() { ) : viewMode === "integrations" ? (
- +
) : viewMode === "mcp" ? ( @@ -78,6 +89,16 @@ function toIsoDate(value: string | Date | null | undefined): string | null { return d.toISOString() } +function toMs(value: string | null | undefined): number { + if (!value) return 0 + const t = new Date(value).getTime() + return Number.isNaN(t) ? 0 : t +} + +function compactRelativeTime(value: number | string): string { + return formatRelativeTime(value).replace(/\s*ago$/i, "") +} + function parsePluginAuthKeys( apiKeys: ListedApiKey[], keyPrefix: (key: ListedApiKey) => string | null, @@ -522,11 +543,18 @@ function NewChip() { ) } -function IconBox({ children }: { children: ReactNode }) { +function IconBox({ + children, + size = "md", +}: { + children: ReactNode + size?: "sm" | "md" +}) { return (
@@ -979,6 +1007,680 @@ function ConnectionsCountPill({ count }: { count: number }) { ) } +const CONNECTOR_META: Record< + ConnectorProvider, + { name: string; icon: ReactNode; documentLabel: string } +> = { + "google-drive": { + name: "Google Drive", + icon: , + documentLabel: "documents", + }, + notion: { + name: "Notion", + icon: , + documentLabel: "pages", + }, + onedrive: { + name: "OneDrive", + icon: , + documentLabel: "documents", + }, +} + +interface PluginEntry { + kind: "plugin" + id: string + name: string + icon: ReactNode + pro: boolean + agentCount: number + createdAt: string | null + lastActive: string | null + onManage: () => void +} + +interface ConnectorEntry { + kind: "connector" + id: string + name: string + documentLabel: string + icon: ReactNode + pro: boolean + provider: ConnectorProvider + connection: Connection + connectionCount: number + email: string | null + spaceName: string | null + createdAt: string | null + onManage: () => void + onReconnect: () => void +} + +type RailEntry = PluginEntry | ConnectorEntry + +function railConnectionMeta(connection: Connection) { + const m = connection.metadata as Record | undefined + return { + syncInProgress: m?.syncInProgress === true, + lastSyncedAt: + typeof m?.lastSyncedAt === "number" ? m.lastSyncedAt : undefined, + documentCount: typeof m?.documentCount === "number" ? m.documentCount : 0, + } +} + +function RailDetail({ label, value }: { label: string; value: ReactNode }) { + return ( +
+ {label} + + {value} + +
+ ) +} + +function RailAction({ + label, + onClick, + danger, +}: { + label: string + onClick: () => void + danger?: boolean +}) { + return ( + + ) +} + +function RailRow({ + icon, + name, + statusLine, + expanded, + onToggle, + children, +}: { + icon: ReactNode + name: string + statusLine: ReactNode + expanded: boolean + onToggle: () => void + children: ReactNode +}) { + return ( +
+ + + {expanded && ( + +
+ {children} +
+
+ )} +
+
+ ) +} + +function ActiveStatusDot() { + return ( + <> + + + Active + + + ) +} + +function PluginRailRow({ entry }: { entry: PluginEntry }) { + const [expanded, setExpanded] = useState(false) + const lastTime = entry.lastActive ?? entry.createdAt + const suffix = [ + entry.agentCount > 1 ? `${entry.agentCount} agents` : null, + lastTime ? formatRelativeTime(lastTime) : null, + ] + .filter(Boolean) + .join(" · ") + return ( + setExpanded((v) => !v)} + statusLine={ +
+ + {suffix && ( + + · {suffix} + + )} +
+ } + > + {entry.createdAt && ( + + )} + {entry.lastActive && ( + + )} + +
+ +
+
+ ) +} + +const CONNECTOR_STATUS = { + expired: { color: "#EF4444", label: "Expired" }, + syncing: { color: "#4BA0FA", label: "Syncing" }, + synced: { color: "#00AC3F", label: "Synced" }, + idle: { color: "#737373", label: "Connected" }, +} as const + +function ConnectorRailRow({ entry }: { entry: ConnectorEntry }) { + const [expanded, setExpanded] = useState(false) + const { needsReauth } = useConnectionHealth(entry.connection.id) + const meta = railConnectionMeta(entry.connection) + const status: keyof typeof CONNECTOR_STATUS = needsReauth + ? "expired" + : meta.syncInProgress + ? "syncing" + : meta.lastSyncedAt + ? "synced" + : "idle" + const { color, label } = CONNECTOR_STATUS[status] + const statusParts = [ + status !== "syncing" && meta.documentCount > 0 + ? String(meta.documentCount) + : null, + status === "synced" && meta.lastSyncedAt + ? compactRelativeTime(meta.lastSyncedAt) + : null, + ].filter(Boolean) + return ( + setExpanded((v) => !v)} + statusLine={ +
+ + + {label} + + {statusParts.length > 0 && ( + + · {statusParts.join(" · ")} + + )} +
+ } + > + {entry.email && } + {entry.createdAt && ( + + )} + {meta.lastSyncedAt && ( + + )} + {meta.documentCount > 0 && ( + + )} + {entry.spaceName && } + {needsReauth && ( + Reconnect needed
} + /> + )} +
+ {needsReauth && ( + + )} + +
+ + ) +} + +const SKELETON_KEYS = ["s1", "s2", "s3", "s4", "s5"] + +function RailSkeleton({ rows }: { rows: number }) { + return ( +
+ {SKELETON_KEYS.slice(0, rows).map((k) => ( +
+
+
+
+
+
+
+ ))} +
+ ) +} + +function RailEmpty({ + icon, + title, + hint, +}: { + icon: ReactNode + title: string + hint: string +}) { + return ( +
+
+ {icon} +
+

+ {title} +

+

+ {hint} +

+
+ ) +} + +function ActiveConnectionsRail({ + entries, + loading, + className, +}: { + entries: RailEntry[] + loading?: boolean + className?: string +}) { + return ( + + ) +} + +type RecentDoc = z.infer< + typeof DocumentsWithMemoriesResponseSchema +>["documents"][number] + +function hostnameOf(url: string | null | undefined): string | null { + if (!url) return null + try { + return new URL(url).hostname.replace(/^www\./, "") + } catch { + return null + } +} + +const CONNECTOR_SMALL_ICON: Record = { + "google-drive": , + notion: , + onedrive: , +} + +function pluginIconNode(iconSrc: string | null): ReactNode { + if (!iconSrc) return + return ( + + ) +} + +function resolveDocSource( + doc: RecentDoc, + connectionSource: Map, +): { label: string; icon: ReactNode } { + const tags = (doc as { containerTags?: unknown }).containerTags + if (Array.isArray(tags)) { + for (const tag of tags) { + if (typeof tag !== "string") continue + const space = detectPluginSpace(tag) + if (space) { + return { label: space.label, icon: pluginIconNode(space.iconSrc) } + } + } + } + if (doc.connectionId) { + const provider = connectionSource.get(doc.connectionId) + if (provider) { + return { + label: CONNECTOR_META[provider].name, + icon: CONNECTOR_SMALL_ICON[provider], + } + } + } + const cc = detectPluginSource( + doc.metadata as Record | null | undefined, + doc.source, + ) + if (cc) { + return { label: cc.label, icon: pluginIconNode(cc.iconSrc) } + } + if (doc.source === "mcp") { + return { label: "MCP", icon: } + } + const type = (doc.type ?? "").toLowerCase() + if (type.includes("notion")) { + return { label: "Notion", icon: } + } + if ( + type.includes("google") || + type.includes("gdrive") || + type.includes("drive") + ) { + return { label: "Google Drive", icon: } + } + if (type.includes("onedrive") || type.includes("microsoft")) { + return { label: "OneDrive", icon: } + } + const host = hostnameOf(doc.url) + if (host) { + return { label: host, icon: } + } + return { + label: "Note", + icon: , + } +} + +function docDisplayTitle(doc: RecentDoc, sourceLabel: string): string { + const t = doc.title?.trim() + if (t && !/^untitled/i.test(t)) return t + const summary = typeof doc.summary === "string" ? doc.summary.trim() : "" + if (summary) { + const line = summary + .split("\n") + .find((l) => l.trim()) + ?.trim() + if (line) return line.length > 80 ? `${line.slice(0, 79)}…` : line + } + return `${sourceLabel} session` +} + +function RecentDocRow({ + doc, + connectionSource, + onOpen, +}: { + doc: RecentDoc + connectionSource: Map + onOpen: () => void +}) { + const { label, icon } = resolveDocSource(doc, connectionSource) + const title = docDisplayTitle(doc, label) + return ( + + ) +} + +function RecentlyAddedCard({ + docs, + connectionSource, + loading, + onOpenDoc, + onViewAll, + className, +}: { + docs: RecentDoc[] + connectionSource: Map + loading?: boolean + onOpenDoc: (doc: RecentDoc) => void + onViewAll: () => void + className?: string +}) { + return ( + + ) +} + function ItemCard({ actionSlot, icon, @@ -1462,12 +2164,15 @@ function SectionRail({ export function IntegrationsView({ publicMode = false, + onOpenDocument, }: { publicMode?: boolean + onOpenDocument?: (doc: RecentDoc) => void }) { const { setViewMode } = useViewMode() const queryClient = useQueryClient() const { org } = useAuth() + const { allProjects } = useContainerTags() const autumn = useCustomer({ queryOptions: { enabled: !publicMode } }) const hasProProduct = !publicMode && hasActivePlan(autumn.data?.subscriptions, "api_pro") @@ -1502,7 +2207,7 @@ export function IntegrationsView({ queryKey: ["plugins"], }) - const { data: connections = [] } = useQuery({ + const { data: connections = [], isLoading: connectionsLoading } = useQuery({ queryKey: ["connections"], queryFn: async () => { const response = await $fetch("@post/connections/list", { @@ -1516,24 +2221,26 @@ export function IntegrationsView({ enabled: !publicMode && hasProProduct, }) - const { data: apiKeys = [], refetch: refetchKeys } = useQuery( - { - queryKey: ["api-keys", org?.id], - queryFn: async () => { - if (!org?.id) return [] - const API_URL = - process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" - const res = await fetch(`${API_URL}/v3/auth/keys`, { - credentials: "include", - }) - if (!res.ok) return [] - const data = (await res.json()) as { keys?: ListedApiKey[] } - return data.keys ?? [] - }, - enabled: !publicMode && !!org?.id, - staleTime: 30 * 1000, + const { + data: apiKeys = [], + refetch: refetchKeys, + isLoading: apiKeysLoading, + } = useQuery({ + queryKey: ["api-keys", org?.id], + queryFn: async () => { + if (!org?.id) return [] + const API_URL = + process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai" + const res = await fetch(`${API_URL}/v3/auth/keys`, { + credentials: "include", + }) + if (!res.ok) return [] + const data = (await res.json()) as { keys?: ListedApiKey[] } + return data.keys ?? [] }, - ) + enabled: !publicMode && !!org?.id, + staleTime: 30 * 1000, + }) const keyPrefix = useCallback((key: ListedApiKey): string | null => { return key.start ?? (key.name?.startsWith("sm_") ? key.name : null) @@ -1587,6 +2294,15 @@ export function IntegrationsView({ return out }, [connections]) + const connectionSource = useMemo(() => { + const m = new Map() + for (const c of connections) { + const p = c.provider as ConnectorProvider + if (p in CONNECTOR_META) m.set(c.id, p) + } + return m + }, [connections]) + const createPluginKeyMutation = useMutation({ mutationFn: async (pluginId: string) => { const API_URL = @@ -1699,6 +2415,7 @@ export function IntegrationsView({ const [category, setCategory] = useQueryState("cat", catParam) const [, setAddDoc] = useQueryState("add", addDocumentParam) + const [, setDocId] = useQueryState("doc", docParam) const [mcpClient, setMcpClient] = useQueryState("mcpClient", parseAsString) const [mcpModalOpen, setMcpModalOpen] = useState(false) const [search, setSearch] = useState("") @@ -1763,6 +2480,120 @@ export function IntegrationsView({ } }, [category, counts, setCategory]) + const railEntries = useMemo(() => { + const getSpaceName = (tag?: string): string | null => { + if (!tag) return null + if (tag === DEFAULT_PROJECT_ID) return "Default" + return allProjects.find((p) => p.containerTag === tag)?.name ?? null + } + const rows: Array<{ ts: number; entry: RailEntry }> = [] + for (const [pluginId, key] of activePluginById) { + const plugin = PLUGIN_CATALOG[pluginId] + if (!plugin) continue + const count = activeCountByPlugin.get(pluginId) ?? 1 + rows.push({ + ts: toMs(key.lastRequest ?? key.createdAt), + entry: { + kind: "plugin", + id: `plugin-${pluginId}`, + name: plugin.name, + icon: ( + {plugin.name} + ), + pro: !FREE_TIER_PLUGIN_IDS.includes(pluginId), + agentCount: count, + createdAt: key.createdAt ?? null, + lastActive: key.lastRequest ?? null, + onManage: () => setConnectedPluginId(pluginId), + }, + }) + } + for (const provider of [ + "google-drive", + "notion", + "onedrive", + ] as ConnectorProvider[]) { + const conns = connectionsByProvider[provider] + const primary = conns[0] + if (!primary) continue + const meta = CONNECTOR_META[provider] + const earliest = conns.reduce((min, c) => { + if (!min) return c.createdAt + return c.createdAt < min ? c.createdAt : min + }, null) + const email = conns.find((c) => c.email)?.email ?? null + rows.push({ + ts: toMs(earliest), + entry: { + kind: "connector", + id: `connector-${provider}`, + name: meta.name, + documentLabel: meta.documentLabel, + icon: meta.icon, + pro: true, + provider, + connection: primary, + connectionCount: conns.length, + email, + spaceName: getSpaceName(primary.containerTags?.[0]), + createdAt: earliest, + onManage: () => void setAddDoc("connect"), + onReconnect: () => addConnectionMutation.mutate(provider), + }, + }) + } + rows.sort((a, b) => b.ts - a.ts) + return rows.map((r) => r.entry) + }, [ + activePluginById, + activeCountByPlugin, + connectionsByProvider, + allProjects, + setAddDoc, + addConnectionMutation, + ]) + + const hasActiveRail = railEntries.length > 0 + + const { data: recentDocs = [], isLoading: recentsLoading } = useQuery({ + queryKey: ["integrations-recent-docs", org?.id], + queryFn: async () => { + const response = await $fetch("@post/documents/documents", { + body: { + page: 1, + limit: 6, + sort: "createdAt", + order: "desc", + containerTags: [], + }, + disableValidation: true, + }) + if (response.error) { + throw new Error( + response.error?.message || "Failed to load recent documents", + ) + } + const data = response.data as z.infer< + typeof DocumentsWithMemoriesResponseSchema + > + return data.documents ?? [] + }, + enabled: !publicMode && !!org?.id, + staleTime: 60 * 1000, + }) + + const railLoading = + !publicMode && (apiKeysLoading || connectionsLoading || isAutumnLoading) + const showRightColumn = + !publicMode && + (hasActiveRail || recentDocs.length > 0 || railLoading || recentsLoading) + const claudeCodeConnected = activePluginById.has("claude_code") const claudeCodeNeedsPro = !isAutumnLoading && !hasProProduct && !isFreeTierPlugin("claude_code") @@ -2196,65 +3027,107 @@ export function IntegrationsView({ return (
-
- {!q && } +
+
+ {!q && } -
-
-
- void setCategory(v)} - counts={counts} - compact={searchExpanded || !!search} - /> +
+
+
+
+
+ void setCategory(v)} + counts={counts} + compact={searchExpanded || !!search} + /> +
+ +
+ {visibleItems.length === 0 ? ( +

+ {q + ? `No integrations match “${search}”.` + : "Nothing in this category yet."} +

+ ) : q || category !== "all" ? ( +
+ {visibleItems.map((item) => renderItemCard(item))} +
+ ) : ( +
+ {SECTION_ORDER.map((cat) => { + const items = visibleItems.filter( + (i) => itemCategory(i) === cat, + ) + if (items.length === 0) return null + return ( + + {items.map((item) => ( +
+ {renderItemCard(item)} +
+ ))} +
+ ) + })} +
+ )} +
- + {showRightColumn && ( +
+
+ + { + if (onOpenDocument) { + onOpenDocument(doc) + return + } + void setDocId(doc.id ?? doc.customId ?? null) + }} + onViewAll={() => setViewMode("list")} + /> +
+
+ )}
- {visibleItems.length === 0 ? ( -

- {q - ? `No integrations match “${search}”.` - : "Nothing in this category yet."} -

- ) : q || category !== "all" ? ( -
- {visibleItems.map((item) => renderItemCard(item))} -
- ) : ( -
- {SECTION_ORDER.map((cat) => { - const items = visibleItems.filter( - (i) => itemCategory(i) === cat, - ) - if (items.length === 0) return null - return ( - - {items.map((item) => ( -
- {renderItemCard(item)} -
- ))} -
- ) - })} -
- )}