Sidebar cleanup.
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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',
|
||||
|
||||
111
wild-web-app/src/components/apps/appUtils.tsx
Normal file
111
wild-web-app/src/components/apps/appUtils.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user