feat(account): added account to settings

This commit is contained in:
Emir Karabeg
2025-03-04 16:39:49 -08:00
parent 9d4b3edbd7
commit 2eb02a3369
11 changed files with 414 additions and 15 deletions

View File

@@ -0,0 +1,253 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { ChevronDown, LogOut, Plus, User, UserPlus } from 'lucide-react'
import { AgentIcon } from '@/components/icons'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { signOut, useSession } from '@/lib/auth-client'
import { cn } from '@/lib/utils'
import { clearUserData } from '@/stores'
interface AccountProps {
onOpenChange: (open: boolean) => void
}
// Mock user data - in a real app, this would come from an auth provider
interface UserData {
isLoggedIn: boolean
name?: string
email?: string
}
interface AccountData {
id: string
name: string
email: string
isActive?: boolean
}
export function Account({ onOpenChange }: AccountProps) {
const router = useRouter()
// In a real app, this would be fetched from an auth provider
const [userData, setUserData] = useState<UserData>({
isLoggedIn: false,
name: '',
email: '',
})
// Get session data using the client hook
const { data: session, isPending, error } = useSession()
const [isLoadingUserData, setIsLoadingUserData] = useState(false)
// Mock accounts for the multi-account UI
const [accounts, setAccounts] = useState<AccountData[]>([])
const [open, setOpen] = useState(false)
// Update user data when session changes
useEffect(() => {
const updateUserData = async () => {
if (!isPending && session?.user) {
// User is logged in
setUserData({
isLoggedIn: true,
name: session.user.name || 'User',
email: session.user.email,
})
setAccounts([
{
id: '1',
name: session.user.name || 'User',
email: session.user.email,
isActive: true,
},
])
} else if (!isPending) {
// User is not logged in
setUserData({
isLoggedIn: false,
name: '',
email: '',
})
setAccounts([])
}
}
updateUserData()
}, [session, isPending])
const handleSignIn = () => {
// Use Next.js router to navigate to login page
router.push('/login')
setOpen(false)
}
const handleSignOut = async () => {
try {
// Start the sign-out process
const signOutPromise = signOut()
// Clear all user data to prevent persistence between accounts
await clearUserData()
// Set a short timeout to improve perceived performance
// while still ensuring auth state starts to clear
setTimeout(() => {
router.push('/login?fromLogout=true')
}, 100)
// Still wait for the promise to resolve/reject to catch errors
await signOutPromise
} catch (error) {
console.error('Error signing out:', error)
// Still navigate even if there's an error
router.push('/login?fromLogout=true')
} finally {
setOpen(false)
}
}
const activeAccount = accounts.find((acc) => acc.isActive) || accounts[0]
// Loading animation component
const LoadingAccountBlock = () => (
<div className="group flex items-center justify-between gap-3 rounded-lg border bg-card p-4 shadow-sm">
<div className="flex items-center gap-3">
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted animate-pulse">
<div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-[shimmer_1.5s_infinite]"
style={{ transform: 'translateX(-100%)', animation: 'shimmer 1.5s infinite' }}
></div>
</div>
<div className="flex flex-col gap-2">
<div className="h-4 w-24 bg-muted rounded animate-pulse"></div>
<div className="h-3 w-32 bg-muted rounded animate-pulse"></div>
</div>
</div>
<div className="h-4 w-4 bg-muted rounded"></div>
</div>
)
return (
<div className="p-6 space-y-6">
<div>
<h3 className="text-lg font-medium mb-4">Account</h3>
</div>
{/* Account Dropdown Component */}
<div className="max-w-xs">
<div className="relative">
{isPending || isLoadingUserData ? (
<LoadingAccountBlock />
) : (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<div
className={cn(
'group flex items-center justify-between gap-3 rounded-lg border bg-card p-4 shadow-sm transition-all cursor-pointer',
'hover:bg-accent/50 hover:shadow-md',
open && 'bg-accent/50 shadow-md'
)}
data-state={open ? 'open' : 'closed'}
>
<div className="flex items-center gap-3">
<div className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-blue-500">
{userData.isLoggedIn ? (
<div className="h-full w-full flex items-center justify-center bg-[#7F2FFF]">
<AgentIcon className="text-white transition-transform duration-200 group-hover:scale-110 -translate-y-[0.5px]" />
</div>
) : (
<div className="bg-gray-500 h-full w-full flex items-center justify-center">
<AgentIcon className="text-white transition-transform duration-200 group-hover:scale-110" />
</div>
)}
{userData.isLoggedIn && accounts.length > 1 && (
<div className="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground">
{accounts.length}
</div>
)}
</div>
<div className="flex flex-col gap-1 mb-[-2px]">
<h3 className="font-medium leading-none truncate max-w-[160px]">
{userData.isLoggedIn ? activeAccount?.name : 'Sign in'}
</h3>
<p className="text-sm text-muted-foreground truncate max-w-[160px]">
{userData.isLoggedIn ? activeAccount?.email : 'Click to sign in'}
</p>
</div>
</div>
<ChevronDown
className={cn(
'h-4 w-4 text-muted-foreground transition-transform',
open && 'rotate-180'
)}
/>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="w-[300px] max-h-[350px] overflow-y-auto"
sideOffset={8}
>
{userData.isLoggedIn ? (
<>
{accounts.length > 1 && (
<>
<div className="mb-2 px-2 py-1.5 text-sm font-medium text-muted-foreground">
Switch Account
</div>
{accounts.map((account) => (
<DropdownMenuItem
key={account.id}
className={cn(
'flex items-center gap-2 p-3 cursor-pointer',
account.isActive && 'bg-accent'
)}
>
<div className="relative flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-[#7F2FFF]">
<User className="text-white w-4 h-4" />
</div>
<div className="flex flex-col">
<span className="font-medium leading-none">{account.name}</span>
<span className="text-xs text-muted-foreground">{account.email}</span>
</div>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem
className="flex items-center gap-2 p-3 cursor-pointer text-destructive focus:text-destructive"
onClick={handleSignOut}
>
<LogOut className="h-4 w-4" />
<span>Sign Out</span>
</DropdownMenuItem>
</>
) : (
<>
<DropdownMenuItem
className="flex items-center gap-2 p-3 cursor-pointer"
onClick={handleSignIn}
>
<UserPlus className="h-4 w-4" />
<span>Sign in</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</div>
</div>
)
}

View File

@@ -243,7 +243,7 @@ export function EnvironmentVariables({ onOpenChange }: EnvironmentVariablesProps
<div className="flex flex-col h-full">
{/* Fixed Header */}
<div className="px-6 pt-6">
<h2 className="text-lg font-medium mb-4">Environment Variables</h2>
<h2 className="text-lg font-medium mb-6">Environment Variables</h2>
<div className={`${GRID_COLS} px-0.5 mb-2`}>
<Label>Key</Label>
<Label>Value</Label>

View File

@@ -40,7 +40,7 @@ export function General() {
return (
<div className="p-6 space-y-6">
<div>
<h2 className="text-lg font-medium mb-4">General Settings</h2>
<h2 className="text-lg font-medium mb-[22px]">General Settings</h2>
<div className="space-y-4">
<div className="flex items-center justify-between py-1">
<Label htmlFor="theme-select" className="font-medium">

View File

@@ -1,9 +1,9 @@
import { KeyRound, Settings } from 'lucide-react'
import { KeyRound, Settings, UserCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
interface SettingsNavigationProps {
activeSection: string
onSectionChange: (section: 'general' | 'environment') => void
onSectionChange: (section: 'general' | 'environment' | 'account') => void
}
const navigationItems = [
@@ -17,6 +17,11 @@ const navigationItems = [
label: 'Environment',
icon: KeyRound,
},
{
id: 'account',
label: 'Account',
icon: UserCircle,
},
] as const
export function SettingsNavigation({ activeSection, onSectionChange }: SettingsNavigationProps) {

View File

@@ -5,6 +5,7 @@ import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
import { Account } from './components/account/account'
import { EnvironmentVariables } from './components/environment/environment'
import { General } from './components/general/general'
import { SettingsNavigation } from './components/settings-navigation/settings-navigation'
@@ -14,7 +15,7 @@ interface SettingsModalProps {
onOpenChange: (open: boolean) => void
}
type SettingsSection = 'general' | 'environment'
type SettingsSection = 'general' | 'environment' | 'account'
export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
const [activeSection, setActiveSection] = useState<SettingsSection>('general')
@@ -51,6 +52,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
<div className={cn('h-full', activeSection === 'environment' ? 'block' : 'hidden')}>
<EnvironmentVariables onOpenChange={onOpenChange} />
</div>
<div className={cn('h-full', activeSection === 'account' ? 'block' : 'hidden')}>
<Account onOpenChange={onOpenChange} />
</div>
</div>
</div>
</DialogContent>

View File

@@ -26,9 +26,9 @@ export function ToolbarBlock({ config }: ToolbarBlockProps) {
}`}
/>
</div>
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 mb-[-2px]">
<h3 className="font-medium leading-none">{config.name}</h3>
<p className="text-sm text-muted-foreground">{config.description}</p>
<p className="text-sm text-muted-foreground leading-snug">{config.description}</p>
</div>
</div>
)

View File

@@ -0,0 +1,25 @@
'use client'
import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
import { cn } from '@/lib/utils'
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
'shrink-0 bg-border',
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
className
)}
{...props}
/>
))
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

47
package-lock.json generated
View File

@@ -21,6 +21,7 @@
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.2",
@@ -2554,6 +2555,52 @@
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz",

View File

@@ -33,6 +33,7 @@
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.2",

View File

@@ -5,7 +5,7 @@ import { useCustomToolsStore } from './custom-tools/store'
import { useExecutionStore } from './execution/store'
import { useNotificationStore } from './notifications/store'
import { useEnvironmentStore } from './settings/environment/store'
import { getSyncManagers, initializeSyncManagers } from './sync-registry'
import { getSyncManagers, initializeSyncManagers, resetSyncManagers } from './sync-registry'
import {
loadRegistry,
loadSubblockValues,
@@ -145,6 +145,31 @@ function cleanupApplication(): void {
getSyncManagers().forEach((manager) => manager.dispose())
}
/**
* Clear all user data when signing out
* This ensures data from one account doesn't persist to another
*/
export async function clearUserData(): Promise<void> {
if (typeof window === 'undefined') return
try {
// 1. Reset all sync managers to prevent any pending syncs
resetSyncManagers()
// 2. Reset all stores to their initial state
resetAllStores()
// 3. Clear localStorage except for essential app settings
const keysToKeep = ['next-favicon', 'theme']
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
keysToRemove.forEach((key) => localStorage.removeItem(key))
console.log('User data cleared successfully')
} catch (error) {
console.error('Error clearing user data:', error)
}
}
/**
* Hook to manage application lifecycle
*/
@@ -159,6 +184,16 @@ export function useAppInitialization() {
}, [])
}
/**
* Hook to reinitialize the application after successful login
* Use this in the login success handler or post-login page
*/
export function useLoginInitialization() {
useEffect(() => {
reinitializeAfterLogin()
}, [])
}
// Initialize immediately when imported on client
if (typeof window !== 'undefined') {
initializeApplication()
@@ -178,13 +213,6 @@ export {
// Helper function to reset all stores
export const resetAllStores = () => {
if (typeof window !== 'undefined') {
// Selectively clear localStorage items
const keysToKeep = ['next-favicon']
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
keysToRemove.forEach((key) => localStorage.removeItem(key))
}
// Reset all stores to initial state
useWorkflowRegistry.setState({
workflows: {},
@@ -229,3 +257,23 @@ export const logAllStores = () => {
// Re-export sync managers
export { workflowSync, environmentSync } from './sync-registry'
/**
* Reinitialize the application after login
* This ensures we load fresh data from the database for the new user
*/
export async function reinitializeAfterLogin(): Promise<void> {
if (typeof window === 'undefined') return
try {
// Reset initialization flags to force a fresh load
isInitializing = false
// Reinitialize the application
await initializeApplication()
console.log('Application reinitialized after login')
} catch (error) {
console.error('Error reinitializing application:', error)
}
}

View File

@@ -69,5 +69,21 @@ export function getSyncManagers(): SyncManager[] {
return managers
}
/**
* Reset all sync managers
* This is used during sign-out to ensure clean state for the next user
*/
export function resetSyncManagers(): void {
// Dispose all existing managers
managers.forEach((manager) => manager.dispose())
// Reset the managers array
managers = []
// Reset initialization flags
initialized = false
initializing = false
}
// Export individual sync managers for direct use
export { workflowSync, environmentSync }