Sidebar cleanup.

This commit is contained in:
2025-11-21 16:16:03 +00:00
parent 6bbf48fe20
commit b324540ce0
4 changed files with 193 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
import { NavLink, useParams } from 'react-router';
import { Server, Play, Container, AppWindow, Settings, CloudLightning, Sun, Moon, Monitor, ChevronDown, Globe, Usb } from 'lucide-react';
import { Server, Play, Container, AppWindow, Settings, CloudLightning, Sun, Moon, Monitor, ChevronDown, Globe, Usb, Download, CheckCircle } from 'lucide-react';
import { cn } from '../lib/utils';
import {
Sidebar,
@@ -71,7 +71,23 @@ export function AppSidebar() {
</div>
</div>
<div className="px-2 group-data-[collapsible=icon]:px-2">
<InstanceSwitcher />
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<InstanceSwitcher />
</div>
<NavLink to={`/instances/${instanceId}/cloud`}>
{({ isActive }) => (
<SidebarMenuButton
isActive={isActive}
tooltip="Configure instance settings"
size="sm"
className="h-8 w-8 p-0"
>
<Settings className="h-4 w-4" />
</SidebarMenuButton>
)}
</NavLink>
</div>
</div>
</SidebarHeader>
@@ -100,29 +116,6 @@ export function AppSidebar() {
</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">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
@@ -177,17 +170,6 @@ export function AppSidebar() {
</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>
</CollapsibleContent>
</SidebarMenuItem>
@@ -225,21 +207,58 @@ export function AppSidebar() {
</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>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Install and manage applications">
<NavLink to={`/instances/${instanceId}/apps`}>
<div className="p-1 rounded-md">
<Collapsible defaultOpen className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<AppWindow className="h-4 w-4" />
</div>
<span className="truncate">Apps</span>
</NavLink>
</SidebarMenuButton>
</SidebarMenuItem>
Apps
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem>
<SidebarMenuSubButton asChild>
<NavLink to={`/instances/${instanceId}/apps/available`}>
<div className="p-1 rounded-md">
<Download className="h-4 w-4" />
</div>
<span className="truncate">Available</span>
</NavLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton asChild>
<NavLink to={`/instances/${instanceId}/apps/installed`}>
<div className="p-1 rounded-md">
<CheckCircle className="h-4 w-4" />
</div>
<span className="truncate">Installed</span>
</NavLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
<SidebarMenuItem>
<SidebarMenuButton asChild tooltip="Advanced settings and system configuration">

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useLocation } from 'react-router';
import { Card } from './ui/card';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
@@ -37,6 +38,7 @@ interface MergedApp extends App {
type TabView = 'available' | 'installed';
export function AppsComponent() {
const location = useLocation();
const { currentInstance } = useInstanceContext();
const { data: availableAppsData, isLoading: loadingAvailable, error: availableError } = useAvailableApps();
const {
@@ -51,7 +53,8 @@ export function AppsComponent() {
isDeleting
} = useDeployedApps(currentInstance);
const [activeTab, setActiveTab] = useState<TabView>('available');
// Determine active tab from URL path
const activeTab: TabView = location.pathname.endsWith('/installed') ? 'installed' : 'available';
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [configDialogOpen, setConfigDialogOpen] = useState(false);
@@ -323,22 +326,6 @@ export function AppsComponent() {
</div>
</div>
{/* Tab Navigation */}
<div className="flex gap-2 mb-6 border-b pb-4">
<Button
variant={activeTab === 'available' ? 'default' : 'outline'}
onClick={() => setActiveTab('available')}
>
Available Apps ({availableApps.length})
</Button>
<Button
variant={activeTab === 'installed' ? 'default' : 'outline'}
onClick={() => setActiveTab('installed')}
>
Installed Apps ({installedApps.length})
</Button>
</div>
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />

View File

@@ -101,7 +101,20 @@ export const routes: RouteObject[] = [
},
{
path: 'apps',
element: <AppsPage />,
children: [
{
index: true,
element: <Navigate to="available" replace />,
},
{
path: 'available',
element: <AppsPage />,
},
{
path: 'installed',
element: <AppsPage />,
},
],
},
{
path: 'advanced',

View File

@@ -0,0 +1,111 @@
import { useState } from 'react';
import {
AppWindow,
Database,
Globe,
Shield,
BarChart3,
MessageSquare,
CheckCircle,
AlertCircle,
Download,
Loader2,
Settings,
} from 'lucide-react';
import { Badge } from '../ui/badge';
import type { App } from '../../services/api';
export interface MergedApp extends App {
deploymentStatus?: 'added' | 'deployed';
url?: string;
}
export function getStatusIcon(status?: string) {
switch (status) {
case 'running':
return <CheckCircle className="h-5 w-5 text-green-500" />;
case 'error':
return <AlertCircle className="h-5 w-5 text-red-500" />;
case 'deploying':
return <Loader2 className="h-5 w-5 text-blue-500 animate-spin" />;
case 'stopped':
return <AlertCircle className="h-5 w-5 text-yellow-500" />;
case 'added':
return <Settings className="h-5 w-5 text-blue-500" />;
case 'deployed':
return <CheckCircle className="h-5 w-5 text-green-500" />;
case 'available':
return <Download className="h-5 w-5 text-muted-foreground" />;
default:
return null;
}
}
export function getStatusBadge(app: MergedApp) {
// Determine status: runtime status > deployment status > available
const status = app.status?.status || app.deploymentStatus || 'available';
const variants: Record<string, 'secondary' | 'default' | 'success' | 'destructive' | 'warning' | 'outline'> = {
available: 'secondary',
added: 'outline',
deploying: 'default',
running: 'success',
error: 'destructive',
stopped: 'warning',
deployed: 'outline',
};
const labels: Record<string, string> = {
available: 'Available',
added: 'Added',
deploying: 'Deploying',
running: 'Running',
error: 'Error',
stopped: 'Stopped',
deployed: 'Deployed',
};
return (
<Badge variant={variants[status]}>
{labels[status] || status}
</Badge>
);
}
export function getCategoryIcon(category?: string) {
switch (category) {
case 'database':
return <Database className="h-4 w-4" />;
case 'web':
return <Globe className="h-4 w-4" />;
case 'security':
return <Shield className="h-4 w-4" />;
case 'monitoring':
return <BarChart3 className="h-4 w-4" />;
case 'communication':
return <MessageSquare className="h-4 w-4" />;
case 'storage':
return <Database className="h-4 w-4" />;
default:
return <AppWindow className="h-4 w-4" />;
}
}
export function AppIcon({ app }: { app: MergedApp }) {
const [imageError, setImageError] = useState(false);
return (
<div className="h-12 w-12 rounded-lg bg-muted flex items-center justify-center overflow-hidden">
{app.icon && !imageError ? (
<img
src={app.icon}
alt={app.name}
className="h-full w-full object-contain p-1"
onError={() => setImageError(true)}
/>
) : (
getCategoryIcon(app.category)
)}
</div>
);
}