| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- import { Outlet, NavLink, useNavigate } from 'react-router-dom'
- import { useAuthStore } from '../stores/authStore'
- import { Workflow, Play, Settings, LogOut, Menu, Database, Code, Key, FolderKanban} from 'lucide-react'
- import { useState, useEffect } from 'react'
- import { useQuery } from '@tanstack/react-query'
- import { useProjectStore } from '../stores/projectStore'
- import { projectsApi } from '../api/users'
- import clsx from 'clsx'
- import { ThemeToggle } from './ThemeToggle'
- export default function Layout() {
- const { user, logout } = useAuthStore()
- const navigate = useNavigate()
- const projectId = useProjectStore((s) => s.projectId)
- const setProject = useProjectStore((s) => s.setProject)
- const { data: projects = [] } = useQuery({
- queryKey: ['projects'],
- queryFn: () => projectsApi.list(),
- staleTime: 5 * 60 * 1000,
- })
- // A project that has gone - deleted, or access removed - would otherwise
- // leave the selector showing a blank and every page filtering to nothing.
- useEffect(() => {
- if (projectId && projects.length && !projects.some((p) => p._id === projectId)) {
- setProject(null)
- }
- }, [projectId, projects, setProject])
- const [sidebarOpen, setSidebarOpen] = useState(false)
- const handleLogout = () => {
- logout()
- navigate('/login')
- }
- const navItems = [
- { to: '/workflows', icon: Workflow, label: 'Workflows' },
- { to: '/executions', icon: Play, label: 'Executions' },
- { to: '/nodes', icon: Code, label: 'Nodes' },
- { to: '/credentials', icon: Key, label: 'Credentials' },
- { to: '/projects', icon: FolderKanban, label: 'Projects' },
- { to: '/database', icon: Database, label: 'Database' },
- { to: '/settings', icon: Settings, label: 'Settings' },
- ]
- return (
- <div className="h-screen bg-gray-50 dark:bg-slate-900 flex transition-colors overflow-hidden">
- {/* Mobile sidebar backdrop */}
- {sidebarOpen && (
- <div
- className="fixed inset-0 bg-black/50 z-20 lg:hidden"
- onClick={() => setSidebarOpen(false)}
- />
- )}
- {/* Sidebar */}
- <aside
- className={clsx(
- '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',
- sidebarOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
- )}
- >
- <div className="flex flex-col h-full">
- {/* Logo */}
- <div className="flex items-center h-16 px-6 border-b border-gray-200 dark:border-slate-700">
- <span className="text-xl font-bold text-primary-600 dark:text-primary-400">SmartBotic</span>
- </div>
- {/* Which project everything below is showing. Above the navigation
- rather than inside a page, because it decides what several pages
- show and used to be visible on only one of them. */}
- <div className="px-4 pt-4">
- <label className="block text-[11px] uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-1">
- Project
- </label>
- <select
- value={projectId ?? ''}
- onChange={(e) => setProject(e.target.value || null)}
- 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"
- title={
- projectId
- ? 'Workflows, folders and executions are limited to this project'
- : 'Showing every project you can reach'
- }
- >
- <option value="">All projects</option>
- {projects.map((p) => (
- <option key={p._id} value={p._id}>{p.name}</option>
- ))}
- </select>
- </div>
- {/* Navigation */}
- <nav className="flex-1 p-4 space-y-1">
- {navItems.map((item) => (
- <NavLink
- key={item.to}
- to={item.to}
- className={({ isActive }) =>
- clsx(
- 'flex items-center gap-3 px-4 py-2 rounded-lg transition-colors',
- isActive
- ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
- : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700'
- )
- }
- onClick={() => setSidebarOpen(false)}
- >
- <item.icon className="w-5 h-5" />
- {item.label}
- </NavLink>
- ))}
- </nav>
- {/* Theme toggle */}
- <div className="px-4 pb-2">
- <ThemeToggle />
- </div>
- {/* User section */}
- <div className="p-4 border-t border-gray-200 dark:border-slate-700">
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-3">
- <div className="w-8 h-8 bg-primary-100 dark:bg-primary-900/50 rounded-full flex items-center justify-center">
- <span className="text-primary-700 dark:text-primary-400 font-medium text-sm">
- {user?.username?.[0]?.toUpperCase()}
- </span>
- </div>
- <div>
- <p className="text-sm font-medium text-gray-900 dark:text-gray-100">{user?.username}</p>
- <p className="text-xs text-gray-500 dark:text-gray-400">{user?.role}</p>
- </div>
- </div>
- <button
- onClick={handleLogout}
- 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"
- title="Logout"
- >
- <LogOut className="w-5 h-5" />
- </button>
- </div>
- </div>
- </div>
- </aside>
- {/* Main content */}
- <div className="flex-1 flex flex-col min-w-0">
- {/* Mobile header */}
- <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">
- <button
- onClick={() => setSidebarOpen(true)}
- className="p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg"
- >
- <Menu className="w-6 h-6" />
- </button>
- <span className="ml-4 text-lg font-semibold text-gray-900 dark:text-gray-100">SmartBotic</span>
- </header>
- {/* Page content */}
- <main className="flex-1 overflow-hidden">
- <Outlet />
- </main>
- </div>
- </div>
- )
- }
|