feat: move auto-updater to Settings > Updater; collapse Proxmox nav by default

- Restore Settings/Updater.tsx with channel selection and update check UI
- Strip updater state/functions/Card from Settings/Proxmox.tsx; update description
- Add Updater to settingsItems and /settings/updater route in App.tsx
- Replace always-open Proxmox NavLink with accordion toggle (expandedSections state)
- Add ChevronDown/RefreshCw to lucide imports; promote useLocation from void call
This commit is contained in:
Shaun Arman 2026-06-12 21:46:52 -05:00
parent 655f8936c9
commit d24f9e2adf
3 changed files with 201 additions and 193 deletions

View File

@ -11,10 +11,12 @@ import {
Plug,
ChevronLeft,
ChevronRight,
ChevronDown,
Sun,
Moon,
Terminal,
FileCode,
RefreshCw,
Server,
Server as ServerIcon,
Settings,
@ -53,6 +55,7 @@ import { ProxmoxHAPage } from "@/pages/Proxmox/HAPage";
import { ProxmoxTasksPage } from "@/pages/Proxmox/TasksPage";
import { ProxmoxCertificatesPage } from "@/pages/Proxmox/CertificatesPage";
import { ProxmoxSettings } from "@/pages/Settings/Proxmox";
import { Updater } from "@/pages/Settings/Updater";
const navItems = [
{ to: "/", icon: Home, label: "Dashboard" },
@ -88,15 +91,17 @@ const settingsItems = [
{ to: "/settings/integrations", icon: Link, label: "Integrations" },
{ to: "/settings/mcp", icon: Plug, label: "MCP Servers" },
{ to: "/settings/security", icon: Shield, label: "Security" },
{ to: "/settings/updater", icon: RefreshCw, label: "Updater" },
{ to: "/settings/proxmox", icon: Settings, label: "Proxmox" },
];
export default function App() {
const [collapsed, setCollapsed] = useState(false);
const [expandedSections, setExpandedSections] = useState<string[]>([]);
const [appVersion, setAppVersion] = useState("");
const { theme, setTheme, setProviders, getActiveProvider } = useSettingsStore();
const cleanupDone = useRef(false);
void useLocation();
const location = useLocation();
useEffect(() => {
getAppVersionCmd().then(setAppVersion).catch(() => {});
@ -171,30 +176,41 @@ export default function App() {
<nav className="flex-1 px-2 py-3 space-y-1">
{navItems.map((item) => {
if (item.children) {
const isExpanded = expandedSections.includes(item.to);
const isActive = location.pathname.startsWith(item.to);
return (
<div key={item.to}>
<NavLink
to={item.to}
className={({ isActive }) =>
`flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`
<button
onClick={() =>
setExpandedSections((prev) =>
prev.includes(item.to)
? prev.filter((t) => t !== item.to)
: [...prev, item.to]
)
}
className={`w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
isActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}
>
<item.icon className="w-4 h-4 shrink-0" />
{!collapsed && <span>{item.label}</span>}
</NavLink>
{!collapsed && (
{!collapsed && (
isExpanded
? <ChevronDown className="w-3 h-3 ml-auto" />
: <ChevronRight className="w-3 h-3 ml-auto" />
)}
</button>
{!collapsed && isExpanded && (
<div className="ml-4 space-y-1 pl-4 border-l border-muted">
{item.children.map((child) => (
<NavLink
key={child.to}
to={child.to}
className={({ isActive }) =>
className={({ isActive: childActive }) =>
`flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors ${
isActive
childActive
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`
@ -298,6 +314,7 @@ export default function App() {
<Route path="/proxmox/ha" element={<ProxmoxHAPage />} />
<Route path="/proxmox/tasks" element={<ProxmoxTasksPage />} />
<Route path="/proxmox/certificates" element={<ProxmoxCertificatesPage />} />
<Route path="/settings/updater" element={<Updater />} />
<Route path="/settings/proxmox" element={<ProxmoxSettings />} />
<Route path="/settings/integrations" element={<Integrations />} />
<Route path="/settings/mcp" element={<MCPServers />} />

View File

@ -4,201 +4,22 @@ import { Label } from '@/components/ui/index';
import { Switch } from '@/components/ui/index';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/index';
import { Button } from '@/components/ui/index';
import { RefreshCw, Check, AlertCircle, Loader } from 'lucide-react';
import {
checkAppUpdatesCmd,
installAppUpdatesCmd,
getUpdateChannelCmd,
setUpdateChannelCmd,
} from '@/lib/tauriCommands';
export function ProxmoxSettings() {
const [autoUpdate, setAutoUpdate] = React.useState(true);
const [updateChannel, setUpdateChannel] = React.useState<'stable' | 'pre-release'>('stable');
const [autoCheck, setAutoCheck] = React.useState(true);
const [lastCheck, setLastCheck] = React.useState<string>('Never');
const [defaultPort, setDefaultPort] = React.useState<string>('8006');
const [connectionTimeout, setConnectionTimeout] = React.useState<string>('30');
const [retryAttempts, setRetryAttempts] = React.useState<string>('3');
const [verifyCertificates, setVerifyCertificates] = React.useState(true);
const [enableCaching, setEnableCaching] = React.useState(true);
const [enableDebug, setEnableDebug] = React.useState(false);
const [checking, setChecking] = React.useState(false);
const [updateAvailable, setUpdateAvailable] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const loadChannel = async () => {
try {
const ch = await getUpdateChannelCmd();
setUpdateChannel(ch as 'stable' | 'pre-release');
} catch {
console.error('Failed to load channel');
}
};
const handleCheckUpdates = async () => {
setChecking(true);
setError(null);
try {
const available = await checkAppUpdatesCmd();
setUpdateAvailable(available);
setLastCheck(new Date().toLocaleString());
} catch {
setError('Failed to check for updates');
} finally {
setChecking(false);
}
};
const handleInstallUpdate = async () => {
try {
await installAppUpdatesCmd();
setUpdateAvailable(false);
} catch {
setError('Failed to install update');
}
};
const handleChannelChange = async (value: string) => {
setUpdateChannel(value as 'stable' | 'pre-release');
try {
await setUpdateChannelCmd(value);
} catch {
setError('Failed to update channel');
}
};
React.useEffect(() => {
void loadChannel();
void handleCheckUpdates();
}, []);
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Proxmox Settings</h1>
<p className="text-muted-foreground">Configure Proxmox Datacenter Manager integration</p>
<p className="text-muted-foreground">Default settings for Proxmox cluster connections</p>
</div>
<Card>
<CardHeader>
<CardTitle>Update Management</CardTitle>
<CardDescription>Configure how Proxmox updates are managed</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="autoUpdate">Auto-check for updates</Label>
<p className="text-sm text-muted-foreground">
Automatically check for new Proxmox updates
</p>
</div>
<Switch
checked={autoUpdate}
onCheckedChange={setAutoUpdate}
/>
</div>
<div className="space-y-2">
<Label htmlFor="updateChannel">Update Channel</Label>
<Select
value={updateChannel}
onValueChange={handleChannelChange}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="stable">Stable</SelectItem>
<SelectItem value="pre-release">Pre-Release</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{updateChannel === 'stable'
? 'Receive only stable, production-ready updates'
: 'Receive pre-release updates with new features (may be less stable)'}
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="autoCheck">Auto-download updates</Label>
<p className="text-sm text-muted-foreground">
Automatically download updates when available
</p>
</div>
<Switch
checked={autoCheck}
onCheckedChange={setAutoCheck}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<Label className="text-base">Last check</Label>
<p className="text-sm text-muted-foreground">
{lastCheck}
</p>
</div>
<Button onClick={handleCheckUpdates} variant="outline">
{checking ? (
<>
<Loader className="mr-2 h-4 w-4 animate-spin" />
Checking...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Check Now
</>
)}
</Button>
</div>
{error && (
<div className="flex items-center space-x-2 rounded-lg bg-destructive/15 p-3 text-destructive">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">{error}</span>
</div>
)}
{updateAvailable ? (
<div className="flex items-center justify-between rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
<div className="flex items-center space-x-3">
<div className="rounded-full bg-green-600 p-1 text-white">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold text-green-900 dark:text-green-100">
Update Available
</div>
<div className="text-sm text-green-700 dark:text-green-300">
A new version is ready to install
</div>
</div>
</div>
<Button onClick={handleInstallUpdate}>
Install Update
</Button>
</div>
) : (
<div className="flex items-center justify-between rounded-lg bg-muted p-4">
<div className="flex items-center space-x-3">
<div className="rounded-full bg-muted-foreground p-1 text-background">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold">Up to Date</div>
<div className="text-sm text-muted-foreground">
You are running the latest version
</div>
</div>
</div>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Cluster Configuration</CardTitle>

View File

@ -0,0 +1,170 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/index';
import { Button } from '@/components/ui/index';
import { RefreshCw, Check, AlertCircle, Loader } from 'lucide-react';
import {
checkAppUpdatesCmd,
installAppUpdatesCmd,
getUpdateChannelCmd,
setUpdateChannelCmd,
} from '@/lib/tauriCommands';
export function Updater() {
const [channel, setChannel] = useState('stable');
const [checking, setChecking] = useState(false);
const [updateAvailable, setUpdateAvailable] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadChannel = async () => {
try {
const ch = await getUpdateChannelCmd();
setChannel(ch);
} catch {
console.error('Failed to load channel');
}
};
const checkForUpdates = async () => {
setChecking(true);
setError(null);
try {
const available = await checkAppUpdatesCmd();
setUpdateAvailable(available);
} catch {
setError('Failed to check for updates');
} finally {
setChecking(false);
}
};
const handleInstallUpdate = async () => {
try {
await installAppUpdatesCmd();
setUpdateAvailable(false);
} catch {
setError('Failed to install update');
}
};
const handleChannelChange = async (newChannel: string) => {
setChannel(newChannel);
try {
await setUpdateChannelCmd(newChannel);
} catch {
setError('Failed to update channel');
}
};
useEffect(() => {
void loadChannel();
void checkForUpdates();
}, []);
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold">Updater</h1>
<p className="text-muted-foreground">Configure application auto-updates</p>
</div>
<Card>
<CardHeader>
<CardTitle>Update Channel</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex space-x-4">
<button
onClick={() => handleChannelChange('stable')}
className={`flex-1 p-4 rounded-lg border-2 transition-all ${
channel === 'stable'
? 'border-primary bg-primary/5'
: 'border-border hover:border-muted-foreground'
}`}
>
<div className="font-semibold">Stable</div>
<div className="text-sm text-muted-foreground">Production-ready releases</div>
</button>
<button
onClick={() => handleChannelChange('pre-release')}
className={`flex-1 p-4 rounded-lg border-2 transition-all ${
channel === 'pre-release'
? 'border-primary bg-primary/5'
: 'border-border hover:border-muted-foreground'
}`}
>
<div className="font-semibold">Pre-Release</div>
<div className="text-sm text-muted-foreground">Latest development builds</div>
</button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle>Check for Updates</CardTitle>
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={checking}
>
{checking ? (
<>
<Loader className="mr-2 h-4 w-4 animate-spin" />
Checking...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Check Now
</>
)}
</Button>
</CardHeader>
<CardContent>
{error && (
<div className="mb-4 flex items-center space-x-2 rounded-lg bg-destructive/15 p-3 text-destructive">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">{error}</span>
</div>
)}
{updateAvailable ? (
<div className="flex items-center justify-between rounded-lg bg-green-50 p-4 dark:bg-green-900/20">
<div className="flex items-center space-x-3">
<div className="rounded-full bg-green-600 p-1 text-white">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold text-green-900 dark:text-green-100">
Update Available
</div>
<div className="text-sm text-green-700 dark:text-green-300">
A new version is ready to install
</div>
</div>
</div>
<Button onClick={handleInstallUpdate}>
Install Update
</Button>
</div>
) : (
<div className="flex items-center justify-between rounded-lg bg-muted p-4">
<div className="flex items-center space-x-3">
<div className="rounded-full bg-muted-foreground p-1 text-background">
<Check className="h-4 w-4" />
</div>
<div>
<div className="font-semibold">Up to Date</div>
<div className="text-sm text-muted-foreground">
You are running the latest version
</div>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
}