Layout.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { Outlet, NavLink, useNavigate } from 'react-router-dom'
  2. import { useAuthStore } from '../stores/authStore'
  3. import { Workflow, Play, Settings, LogOut, Menu, Database, Code, Key, FolderKanban} from 'lucide-react'
  4. import { useState, useEffect } from 'react'
  5. import { useQuery } from '@tanstack/react-query'
  6. import { useProjectStore } from '../stores/projectStore'
  7. import { projectsApi } from '../api/users'
  8. import clsx from 'clsx'
  9. import { ThemeToggle } from './ThemeToggle'
  10. export default function Layout() {
  11. const { user, logout } = useAuthStore()
  12. const navigate = useNavigate()
  13. const projectId = useProjectStore((s) => s.projectId)
  14. const setProject = useProjectStore((s) => s.setProject)
  15. const { data: projects = [] } = useQuery({
  16. queryKey: ['projects'],
  17. queryFn: () => projectsApi.list(),
  18. staleTime: 5 * 60 * 1000,
  19. })
  20. // A project that has gone - deleted, or access removed - would otherwise
  21. // leave the selector showing a blank and every page filtering to nothing.
  22. useEffect(() => {
  23. if (projectId && projects.length && !projects.some((p) => p._id === projectId)) {
  24. setProject(null)
  25. }
  26. }, [projectId, projects, setProject])
  27. const [sidebarOpen, setSidebarOpen] = useState(false)
  28. const handleLogout = () => {
  29. logout()
  30. navigate('/login')
  31. }
  32. const navItems = [
  33. { to: '/workflows', icon: Workflow, label: 'Workflows' },
  34. { to: '/executions', icon: Play, label: 'Executions' },
  35. { to: '/nodes', icon: Code, label: 'Nodes' },
  36. { to: '/credentials', icon: Key, label: 'Credentials' },
  37. { to: '/projects', icon: FolderKanban, label: 'Projects' },
  38. { to: '/database', icon: Database, label: 'Database' },
  39. { to: '/settings', icon: Settings, label: 'Settings' },
  40. ]
  41. return (
  42. <div className="h-screen bg-gray-50 dark:bg-slate-900 flex transition-colors overflow-hidden">
  43. {/* Mobile sidebar backdrop */}
  44. {sidebarOpen && (
  45. <div
  46. className="fixed inset-0 bg-black/50 z-20 lg:hidden"
  47. onClick={() => setSidebarOpen(false)}
  48. />
  49. )}
  50. {/* Sidebar */}
  51. <aside
  52. className={clsx(
  53. 'fixed lg:static inset-y-0 left-0 z-30 w-64 bg-white dark:bg-slate-800 border-r border-gray-200 dark:border-slate-700 transform transition-transform lg:transform-none',
  54. sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
  55. )}
  56. >
  57. <div className="flex flex-col h-full">
  58. {/* Logo */}
  59. <div className="flex items-center h-16 px-6 border-b border-gray-200 dark:border-slate-700">
  60. <span className="text-xl font-bold text-primary-600 dark:text-primary-400">SmartBotic</span>
  61. </div>
  62. {/* Which project everything below is showing. Above the navigation
  63. rather than inside a page, because it decides what several pages
  64. show and used to be visible on only one of them. */}
  65. <div className="px-4 pt-4">
  66. <label className="block text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-1">
  67. Project
  68. </label>
  69. <select
  70. value={projectId ?? ''}
  71. onChange={(e) => setProject(e.target.value || null)}
  72. className="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100"
  73. title={
  74. projectId
  75. ? 'Workflows, folders and executions are limited to this project'
  76. : 'Showing every project you can reach'
  77. }
  78. >
  79. <option value="">All projects</option>
  80. {projects.map((p) => (
  81. <option key={p._id} value={p._id}>{p.name}</option>
  82. ))}
  83. </select>
  84. </div>
  85. {/* Navigation */}
  86. <nav className="flex-1 p-4 space-y-1">
  87. {navItems.map((item) => (
  88. <NavLink
  89. key={item.to}
  90. to={item.to}
  91. className={({ isActive }) =>
  92. clsx(
  93. 'flex items-center gap-3 px-4 py-2 rounded-lg transition-colors',
  94. isActive
  95. ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
  96. : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
  97. )
  98. }
  99. onClick={() => setSidebarOpen(false)}
  100. >
  101. <item.icon className="w-5 h-5" />
  102. {item.label}
  103. </NavLink>
  104. ))}
  105. </nav>
  106. {/* Theme toggle */}
  107. <div className="px-4 pb-2">
  108. <ThemeToggle />
  109. </div>
  110. {/* User section */}
  111. <div className="p-4 border-t border-gray-200 dark:border-slate-700">
  112. <div className="flex items-center justify-between">
  113. <div className="flex items-center gap-3">
  114. <div className="w-8 h-8 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
  115. <span className="text-primary-700 dark:text-primary-400 font-medium text-sm">
  116. {user?.username?.[0]?.toUpperCase()}
  117. </span>
  118. </div>
  119. <div>
  120. <p className="text-sm font-medium text-gray-900 dark:text-gray-100">{user?.username}</p>
  121. <p className="text-xs text-gray-500 dark:text-gray-400">{user?.role}</p>
  122. </div>
  123. </div>
  124. <button
  125. onClick={handleLogout}
  126. className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700"
  127. title="Logout"
  128. >
  129. <LogOut className="w-5 h-5" />
  130. </button>
  131. </div>
  132. </div>
  133. </div>
  134. </aside>
  135. {/* Main content */}
  136. <div className="flex-1 flex flex-col min-w-0">
  137. {/* Mobile header */}
  138. <header className="lg:hidden flex items-center h-16 px-4 bg-white dark:bg-slate-800 border-b border-gray-200 dark:border-slate-700">
  139. <button
  140. onClick={() => setSidebarOpen(true)}
  141. className="p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg"
  142. >
  143. <Menu className="w-6 h-6" />
  144. </button>
  145. <span className="ml-4 text-lg font-semibold text-gray-900 dark:text-gray-100">SmartBotic</span>
  146. </header>
  147. {/* Page content */}
  148. <main className="flex-1 overflow-hidden">
  149. <Outlet />
  150. </main>
  151. </div>
  152. </div>
  153. )
  154. }