Sidebar cleanup.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { NavLink, useParams } from 'react-router';
|
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 { cn } from '../lib/utils';
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
@@ -71,7 +71,23 @@ export function AppSidebar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 group-data-[collapsible=icon]:px-2">
|
<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>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
@@ -100,29 +116,6 @@ export function AppSidebar() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</SidebarMenuItem>
|
</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">
|
<Collapsible defaultOpen className="group/collapsible">
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
@@ -177,17 +170,6 @@ export function AppSidebar() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem> */}
|
</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>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -225,21 +207,58 @@ export function AppSidebar() {
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</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>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
||||||
<SidebarMenuItem>
|
<Collapsible defaultOpen className="group/collapsible">
|
||||||
<SidebarMenuButton asChild tooltip="Install and manage applications">
|
<SidebarMenuItem>
|
||||||
<NavLink to={`/instances/${instanceId}/apps`}>
|
<CollapsibleTrigger asChild>
|
||||||
<div className="p-1 rounded-md">
|
<SidebarMenuButton>
|
||||||
<AppWindow className="h-4 w-4" />
|
<AppWindow className="h-4 w-4" />
|
||||||
</div>
|
Apps
|
||||||
<span className="truncate">Apps</span>
|
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
|
||||||
</NavLink>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuButton>
|
</CollapsibleTrigger>
|
||||||
</SidebarMenuItem>
|
<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>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton asChild tooltip="Advanced settings and system configuration">
|
<SidebarMenuButton asChild tooltip="Advanced settings and system configuration">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
import { Card } from './ui/card';
|
import { Card } from './ui/card';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Badge } from './ui/badge';
|
import { Badge } from './ui/badge';
|
||||||
@@ -37,6 +38,7 @@ interface MergedApp extends App {
|
|||||||
type TabView = 'available' | 'installed';
|
type TabView = 'available' | 'installed';
|
||||||
|
|
||||||
export function AppsComponent() {
|
export function AppsComponent() {
|
||||||
|
const location = useLocation();
|
||||||
const { currentInstance } = useInstanceContext();
|
const { currentInstance } = useInstanceContext();
|
||||||
const { data: availableAppsData, isLoading: loadingAvailable, error: availableError } = useAvailableApps();
|
const { data: availableAppsData, isLoading: loadingAvailable, error: availableError } = useAvailableApps();
|
||||||
const {
|
const {
|
||||||
@@ -51,7 +53,8 @@ export function AppsComponent() {
|
|||||||
isDeleting
|
isDeleting
|
||||||
} = useDeployedApps(currentInstance);
|
} = 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 [searchTerm, setSearchTerm] = useState('');
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
const [selectedCategory, setSelectedCategory] = useState<string>('all');
|
||||||
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
const [configDialogOpen, setConfigDialogOpen] = useState(false);
|
||||||
@@ -323,22 +326,6 @@ export function AppsComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</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="flex flex-col sm:flex-row gap-4 mb-6">
|
||||||
<div className="relative flex-1">
|
<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" />
|
<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',
|
path: 'apps',
|
||||||
element: <AppsPage />,
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <Navigate to="available" replace />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'available',
|
||||||
|
element: <AppsPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'installed',
|
||||||
|
element: <AppsPage />,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'advanced',
|
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