First swing.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { CheckCircle, Lock, Server, Play, Container, AppWindow, Settings, CloudLightning, Sun, Moon, Monitor, ChevronDown, Globe, Wifi, HardDrive } from 'lucide-react';
|
||||
import { NavLink, useParams } from 'react-router';
|
||||
import { Server, Play, Container, AppWindow, Settings, CloudLightning, Sun, Moon, Monitor, ChevronDown, Globe, Wifi, HardDrive, Usb } from 'lucide-react';
|
||||
import { cn } from '../lib/utils';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -16,18 +17,9 @@ import {
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
export type Phase = 'setup' | 'infrastructure' | 'cluster' | 'apps';
|
||||
export type Tab = Phase | 'advanced' | 'cloud' | 'central' | 'dns' | 'dhcp' | 'pxe';
|
||||
|
||||
interface AppSidebarProps {
|
||||
currentTab: Tab;
|
||||
onTabChange: (tab: Tab) => void;
|
||||
completedPhases: Phase[];
|
||||
}
|
||||
|
||||
|
||||
export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSidebarProps) {
|
||||
export function AppSidebar() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
const { instanceId } = useParams<{ instanceId: string }>();
|
||||
|
||||
const cycleTheme = () => {
|
||||
if (theme === 'light') {
|
||||
@@ -61,45 +53,10 @@ export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSide
|
||||
}
|
||||
};
|
||||
|
||||
const getTabStatus = (tab: Tab) => {
|
||||
// Non-phase tabs (like advanced and cloud) are always available
|
||||
if (tab === 'advanced' || tab === 'cloud') {
|
||||
return 'available';
|
||||
}
|
||||
|
||||
// Central sub-tabs are available if setup phase is available or completed
|
||||
if (tab === 'central' || tab === 'dns' || tab === 'dhcp' || tab === 'pxe') {
|
||||
if (completedPhases.includes('setup')) {
|
||||
return 'completed';
|
||||
}
|
||||
return 'available';
|
||||
}
|
||||
|
||||
// For phase tabs, check completion status
|
||||
if (completedPhases.includes(tab as Phase)) {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
// Allow access to the first phase always
|
||||
if (tab === 'setup') {
|
||||
return 'available';
|
||||
}
|
||||
|
||||
// Allow access to the next phase if the previous phase is completed
|
||||
if (tab === 'infrastructure' && completedPhases.includes('setup')) {
|
||||
return 'available';
|
||||
}
|
||||
|
||||
if (tab === 'cluster' && completedPhases.includes('infrastructure')) {
|
||||
return 'available';
|
||||
}
|
||||
|
||||
if (tab === 'apps' && completedPhases.includes('cluster')) {
|
||||
return 'available';
|
||||
}
|
||||
|
||||
return 'locked';
|
||||
};
|
||||
// If no instanceId, we're not in an instance context
|
||||
if (!instanceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar variant="sidebar" collapsible="icon">
|
||||
@@ -110,40 +67,57 @@ export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSide
|
||||
</div>
|
||||
<div className="group-data-[collapsible=icon]:hidden">
|
||||
<h2 className="text-lg font-bold text-foreground">Wild Cloud</h2>
|
||||
<p className="text-sm text-muted-foreground">Central</p>
|
||||
<p className="text-sm text-muted-foreground">{instanceId}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={currentTab === 'cloud'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('cloud');
|
||||
if (status !== 'locked') onTabChange('cloud');
|
||||
}}
|
||||
disabled={getTabStatus('cloud') === 'locked'}
|
||||
tooltip="Configure cloud settings and domains"
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('cloud') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
<NavLink to={`/instances/${instanceId}/dashboard`}>
|
||||
{({ isActive }) => (
|
||||
<SidebarMenuButton
|
||||
isActive={isActive}
|
||||
tooltip="Instance dashboard and overview"
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
isActive && "bg-primary/10"
|
||||
)}>
|
||||
<CloudLightning className={cn(
|
||||
"h-4 w-4",
|
||||
isActive && "text-primary",
|
||||
!isActive && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Dashboard</span>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'cloud' && "bg-primary/10",
|
||||
getTabStatus('cloud') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<CloudLightning className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'cloud' && "text-primary",
|
||||
currentTab !== 'cloud' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Cloud</span>
|
||||
</SidebarMenuButton>
|
||||
</NavLink>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<NavLink to={`/instances/${instanceId}/cloud`}>
|
||||
{({ isActive }) => (
|
||||
<SidebarMenuButton
|
||||
isActive={isActive}
|
||||
tooltip="Configure cloud settings and domains"
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
isActive && "bg-primary/10"
|
||||
)}>
|
||||
<CloudLightning className={cn(
|
||||
"h-4 w-4",
|
||||
isActive && "text-primary",
|
||||
!isActive && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Cloud</span>
|
||||
</SidebarMenuButton>
|
||||
)}
|
||||
</NavLink>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<Collapsible defaultOpen className="group/collapsible">
|
||||
@@ -158,110 +132,57 @@ export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSide
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
isActive={currentTab === 'central'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('central');
|
||||
if (status !== 'locked') onTabChange('central');
|
||||
}}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('central') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'central' && "bg-primary/10",
|
||||
getTabStatus('central') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<Server className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'central' && "text-primary",
|
||||
currentTab !== 'central' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Central</span>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<NavLink to={`/instances/${instanceId}/central`} className={({ isActive }) => isActive ? "data-[active=true]" : ""}>
|
||||
<div className="p-1 rounded-md">
|
||||
<Server className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">Central</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
isActive={currentTab === 'dns'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('dns');
|
||||
if (status !== 'locked') onTabChange('dns');
|
||||
}}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('dns') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'dns' && "bg-primary/10",
|
||||
getTabStatus('dns') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<Globe className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'dns' && "text-primary",
|
||||
currentTab !== 'dns' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">DNS</span>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<NavLink to={`/instances/${instanceId}/dns`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<Globe className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">DNS</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
isActive={currentTab === 'dhcp'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('dhcp');
|
||||
if (status !== 'locked') onTabChange('dhcp');
|
||||
}}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('dhcp') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'dhcp' && "bg-primary/10",
|
||||
getTabStatus('dhcp') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<Wifi className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'dhcp' && "text-primary",
|
||||
currentTab !== 'dhcp' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">DHCP</span>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<NavLink to={`/instances/${instanceId}/dhcp`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<Wifi className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">DHCP</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
isActive={currentTab === 'pxe'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('pxe');
|
||||
if (status !== 'locked') onTabChange('pxe');
|
||||
}}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('pxe') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'pxe' && "bg-primary/10",
|
||||
getTabStatus('pxe') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<HardDrive className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'pxe' && "text-primary",
|
||||
currentTab !== 'pxe' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">PXE</span>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<NavLink to={`/instances/${instanceId}/pxe`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">PXE</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<NavLink to={`/instances/${instanceId}/iso`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<Usb className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">ISO / USB</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
@@ -281,56 +202,24 @@ export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSide
|
||||
<CollapsibleContent>
|
||||
<SidebarMenuSub>
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
isActive={currentTab === 'infrastructure'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('infrastructure');
|
||||
if (status !== 'locked') onTabChange('infrastructure');
|
||||
}}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('infrastructure') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'infrastructure' && "bg-primary/10",
|
||||
getTabStatus('infrastructure') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<Play className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'infrastructure' && "text-primary",
|
||||
currentTab !== 'infrastructure' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Cluster Nodes</span>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<NavLink to={`/instances/${instanceId}/infrastructure`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<Play className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">Cluster Nodes</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
|
||||
<SidebarMenuSubItem>
|
||||
<SidebarMenuSubButton
|
||||
isActive={currentTab === 'cluster'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('cluster');
|
||||
if (status !== 'locked') onTabChange('cluster');
|
||||
}}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('cluster') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'cluster' && "bg-primary/10",
|
||||
getTabStatus('cluster') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<Container className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'cluster' && "text-primary",
|
||||
currentTab !== 'cluster' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Cluster Services</span>
|
||||
<SidebarMenuSubButton asChild>
|
||||
<NavLink to={`/instances/${instanceId}/cluster`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<Container className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">Cluster Services</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
@@ -339,60 +228,24 @@ export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSide
|
||||
</Collapsible>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={currentTab === 'apps'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('apps');
|
||||
if (status !== 'locked') onTabChange('apps');
|
||||
}}
|
||||
disabled={getTabStatus('apps') === 'locked'}
|
||||
tooltip="Install and manage applications"
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('apps') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'apps' && "bg-primary/10",
|
||||
getTabStatus('apps') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<AppWindow className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'apps' && "text-primary",
|
||||
currentTab !== 'apps' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Apps</span>
|
||||
<SidebarMenuButton asChild tooltip="Install and manage applications">
|
||||
<NavLink to={`/instances/${instanceId}/apps`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<AppWindow className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">Apps</span>
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={currentTab === 'advanced'}
|
||||
onClick={() => {
|
||||
const status = getTabStatus('advanced');
|
||||
if (status !== 'locked') onTabChange('advanced');
|
||||
}}
|
||||
disabled={getTabStatus('advanced') === 'locked'}
|
||||
tooltip="Advanced settings and system configuration"
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
getTabStatus('advanced') === 'locked' && "opacity-50 cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"p-1 rounded-md",
|
||||
currentTab === 'advanced' && "bg-primary/10",
|
||||
getTabStatus('advanced') === 'locked' && "bg-muted"
|
||||
)}>
|
||||
<Settings className={cn(
|
||||
"h-4 w-4",
|
||||
currentTab === 'advanced' && "text-primary",
|
||||
currentTab !== 'advanced' && "text-muted-foreground"
|
||||
)} />
|
||||
</div>
|
||||
<span className="truncate">Advanced</span>
|
||||
<SidebarMenuButton asChild tooltip="Advanced settings and system configuration">
|
||||
<NavLink to={`/instances/${instanceId}/advanced`}>
|
||||
<div className="p-1 rounded-md">
|
||||
<Settings className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="truncate">Advanced</span>
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
@@ -413,4 +266,4 @@ export function AppSidebar({ currentTab, onTabChange, completedPhases }: AppSide
|
||||
<SidebarRail/>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user