Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 59 additions & 46 deletions src/app/_components/FilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, { useState } from "react";
import { Button } from "./ui/button";
import { Package, Monitor, Wrench, Server, FileText, Calendar } from "lucide-react";

export interface FilterState {
searchQuery: string;
Expand All @@ -20,10 +21,10 @@ interface FilterBarProps {
}

const SCRIPT_TYPES = [
{ value: "ct", label: "LXC Container", icon: "πŸ“¦" },
{ value: "vm", label: "Virtual Machine", icon: "πŸ’»" },
{ value: "addon", label: "Add-on", icon: "πŸ”§" },
{ value: "pve", label: "PVE Host", icon: "πŸ–₯️" },
{ value: "ct", label: "LXC Container", Icon: Package },
{ value: "vm", label: "Virtual Machine", Icon: Monitor },
{ value: "addon", label: "Add-on", Icon: Wrench },
{ value: "pve", label: "PVE Host", Icon: Server },
];

export function FilterBar({
Expand Down Expand Up @@ -183,38 +184,41 @@ export function FilterBar({
{isTypeDropdownOpen && (
<div className="absolute top-full left-0 z-10 mt-1 w-48 rounded-lg border border-border bg-card shadow-lg">
<div className="p-2">
{SCRIPT_TYPES.map((type) => (
<label
key={type.value}
className="flex cursor-pointer items-center space-x-3 rounded-md px-3 py-2 hover:bg-accent"
>
<input
type="checkbox"
checked={filters.selectedTypes.includes(type.value)}
onChange={(e) => {
if (e.target.checked) {
updateFilters({
selectedTypes: [
...filters.selectedTypes,
type.value,
],
});
} else {
updateFilters({
selectedTypes: filters.selectedTypes.filter(
(t) => t !== type.value,
),
});
}
}}
className="rounded border-input text-primary focus:ring-primary"
/>
<span className="text-lg">{type.icon}</span>
<span className="text-sm text-muted-foreground">
{type.label}
</span>
</label>
))}
{SCRIPT_TYPES.map((type) => {
const IconComponent = type.Icon;
return (
<label
key={type.value}
className="flex cursor-pointer items-center space-x-3 rounded-md px-3 py-2 hover:bg-accent"
>
<input
type="checkbox"
checked={filters.selectedTypes.includes(type.value)}
onChange={(e) => {
if (e.target.checked) {
updateFilters({
selectedTypes: [
...filters.selectedTypes,
type.value,
],
});
} else {
updateFilters({
selectedTypes: filters.selectedTypes.filter(
(t) => t !== type.value,
),
});
}
}}
className="rounded border-input text-primary focus:ring-primary"
/>
<IconComponent className="h-4 w-4" />
<span className="text-sm text-muted-foreground">
{type.label}
</span>
</label>
);
})}
</div>
<div className="border-t border-border p-2">
<Button
Expand All @@ -236,16 +240,25 @@ export function FilterBar({
{/* Sort Options */}
<div className="flex items-center space-x-2">
{/* Sort By Dropdown */}
<select
value={filters.sortBy}
onChange={(e) =>
updateFilters({ sortBy: e.target.value as "name" | "created" })
}
className="rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-primary focus:outline-none"
>
<option value="name">πŸ“ By Name</option>
<option value="created">πŸ“… By Created Date</option>
</select>
<div className="relative inline-flex items-center">
<select
value={filters.sortBy}
onChange={(e) =>
updateFilters({ sortBy: e.target.value as "name" | "created" })
}
className="rounded-lg border border-input bg-background pl-9 pr-3 py-2 text-sm text-foreground focus:ring-2 focus:ring-primary focus:outline-none appearance-none"
>
<option value="name">By Name</option>
<option value="created">By Created Date</option>
</select>
<div className="absolute left-2 pointer-events-none">
{filters.sortBy === "name" ? (
<FileText className="h-4 w-4 text-muted-foreground" />
) : (
<Calendar className="h-4 w-4 text-muted-foreground" />
)}
</div>
</div>

{/* Sort Order Button */}
<Button
Expand Down
6 changes: 3 additions & 3 deletions src/app/_components/ScriptDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,20 @@ export function ScriptDetailModal({
if (data.success) {
const message =
"message" in data ? data.message : "Script loaded successfully";
setLoadMessage(`βœ… ${message}`);
setLoadMessage(`[SUCCESS] ${message}`);
// Refetch script files status and comparison data to update the UI
void refetchScriptFiles();
void refetchComparison();
} else {
const error = "error" in data ? data.error : "Failed to load script";
setLoadMessage(`❌ ${error}`);
setLoadMessage(`[ERROR] ${error}`);
}
// Clear message after 5 seconds
setTimeout(() => setLoadMessage(null), 5000);
},
onError: (error) => {
setIsLoading(false);
setLoadMessage(`❌ Error: ${error.message}`);
setLoadMessage(`[ERROR] ${error.message}`);
setTimeout(() => setLoadMessage(null), 5000);
},
});
Expand Down
21 changes: 13 additions & 8 deletions src/app/_components/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useEffect, useRef, useState } from 'react';
import '@xterm/xterm/css/xterm.css';
import { Button } from './ui/button';
import { Play, Square, Trash2, X } from 'lucide-react';

interface TerminalProps {
scriptPath: string;
Expand Down Expand Up @@ -215,7 +216,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate

switch (message.type) {
case 'start':
xtermRef.current.writeln(`${prefix}πŸš€ ${message.data}`);
xtermRef.current.writeln(`${prefix}[START] ${message.data}`);
setIsRunning(true);
break;
case 'output':
Expand All @@ -232,14 +233,14 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
xtermRef.current.write(message.data);
} else if (message.data.includes('exit code') && message.data.includes('clear')) {
// This is a script error, show it with error prefix
xtermRef.current.writeln(`${prefix}❌ ${message.data}`);
xtermRef.current.writeln(`${prefix}[ERROR] ${message.data}`);
} else {
// This is a real error, show it with error prefix
xtermRef.current.writeln(`${prefix}❌ ${message.data}`);
xtermRef.current.writeln(`${prefix}[ERROR] ${message.data}`);
}
break;
case 'end':
xtermRef.current.writeln(`${prefix}βœ… ${message.data}`);
xtermRef.current.writeln(`${prefix}[SUCCESS] ${message.data}`);
setIsRunning(false);
break;
}
Expand Down Expand Up @@ -337,7 +338,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
size="sm"
className={isConnected && !isRunning ? 'bg-green-600 hover:bg-green-700' : 'bg-muted text-muted-foreground cursor-not-allowed'}
>
▢️ Start
<Play className="h-4 w-4 mr-1" />
Start
</Button>

<Button
Expand All @@ -347,7 +349,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
size="sm"
className={isRunning ? 'bg-red-600 hover:bg-red-700' : 'bg-muted text-muted-foreground cursor-not-allowed'}
>
⏹️ Stop
<Square className="h-4 w-4 mr-1" />
Stop
</Button>

<Button
Expand All @@ -356,7 +359,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
size="sm"
className="bg-secondary text-secondary-foreground hover:bg-secondary/80"
>
πŸ—‘οΈ Clear
<Trash2 className="h-4 w-4 mr-1" />
Clear
</Button>
</div>

Expand All @@ -366,7 +370,8 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
size="sm"
className="bg-gray-600 text-white hover:bg-gray-700"
>
βœ• Close
<X className="h-4 w-4 mr-1" />
Close
</Button>
</div>
</div>
Expand Down
7 changes: 4 additions & 3 deletions src/app/_components/VersionDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { api } from "~/trpc/react";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
import { ExternalLink, Download, RefreshCw, Loader2, Check } from "lucide-react";
import { useState } from "react";

// Loading overlay component
Expand Down Expand Up @@ -223,8 +223,9 @@ export function VersionDisplay() {
)}

{isUpToDate && (
<span className="text-xs text-green-600 dark:text-green-400">
βœ“ Up to date
<span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
<Check className="h-3 w-3" />
Up to date
</span>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const handler = (req: NextRequest) =>
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
`[ERROR] tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
Expand Down
21 changes: 13 additions & 8 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Terminal } from './_components/Terminal';
import { SettingsButton } from './_components/SettingsButton';
import { VersionDisplay } from './_components/VersionDisplay';
import { Button } from './_components/ui/button';
import { Rocket, Package, HardDrive, FolderOpen } from 'lucide-react';

export default function Home() {
const [runningScript, setRunningScript] = useState<{ path: string; name: string; mode?: 'local' | 'ssh'; server?: any } | null>(null);
Expand All @@ -28,8 +29,9 @@ export default function Home() {
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-foreground mb-2">
πŸš€ PVE Scripts Management
<h1 className="text-4xl font-bold text-foreground mb-2 flex items-center justify-center gap-3">
<Rocket className="h-9 w-9" />
PVE Scripts Management
</h1>
<p className="text-muted-foreground mb-4">
Manage and execute Proxmox helper scripts locally with live output streaming
Expand Down Expand Up @@ -59,34 +61,37 @@ export default function Home() {
variant="ghost"
size="null"
onClick={() => setActiveTab('scripts')}
className={`px-3 py-1 text-sm ${
className={`px-3 py-1 text-sm flex items-center gap-2 ${
activeTab === 'scripts'
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent hover:text-accent-foreground'
}`}>
πŸ“¦ Available Scripts
<Package className="h-4 w-4" />
Available Scripts
</Button>
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('downloaded')}
className={`px-3 py-1 text-sm ${
className={`px-3 py-1 text-sm flex items-center gap-2 ${
activeTab === 'downloaded'
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent hover:text-accent-foreground'
}`}>
πŸ’Ύ Downloaded Scripts
<HardDrive className="h-4 w-4" />
Downloaded Scripts
</Button>
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('installed')}
className={`px-3 py-1 text-sm ${
className={`px-3 py-1 text-sm flex items-center gap-2 ${
activeTab === 'installed'
? 'bg-accent text-accent-foreground'
: 'hover:bg-accent hover:text-accent-foreground'
}`}>
πŸ—‚οΈ Installed Scripts
<FolderOpen className="h-4 w-4" />
Installed Scripts
</Button>
</nav>
</div>
Expand Down