Files
fleetforge/apps/web/src/app/downloads/page.tsx
T

416 lines
13 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { api } from "@/lib/api";
import DashboardLayout from "@/components/DashboardLayout";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Download,
Terminal,
MonitorSmartphone,
Apple,
Copy,
Check,
Loader2,
Shield,
} from "lucide-react";
interface PlatformInfo {
os: string;
arch: string;
label: string;
filename: string;
available: boolean;
size: number;
sizeFormatted: string;
installHint: string;
}
interface AgentInfo {
version: string;
platforms: PlatformInfo[];
}
const osIcons: Record<string, typeof Terminal> = {
linux: Terminal,
windows: MonitorSmartphone,
darwin: Apple,
};
function getApiBase() {
return process.env.NEXT_PUBLIC_API_URL || "http://localhost:3002/api";
}
function WindowsInstallerCard() {
const [serverUrl, setServerUrl] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
useEffect(() => {
setServerUrl(getApiBase().replace(/\/api$/, ""));
}, []);
const handleGenerate = async () => {
setLoading(true);
setError(null);
setSuccess(false);
try {
const token = api.getToken();
const res = await fetch(`${getApiBase()}/v1/installers/generate`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ serverUrl }),
});
if (!res.ok) {
let message = `Failed to generate installer (${res.status})`;
try {
const data = await res.json();
if (data?.error?.message) message = data.error.message;
} catch {}
setError(message);
return;
}
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = "forge-agent.msi";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
setSuccess(true);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to generate installer"
);
} finally {
setLoading(false);
}
};
return (
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MonitorSmartphone className="h-5 w-5" />
Windows MSI Installer
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="server-url">Server URL</Label>
<Input
id="server-url"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
placeholder="http://your-server:3002"
/>
<p className="text-xs text-muted-foreground">
The installer will be pre-configured to point the agent at this
Fleet Forge server.
</p>
</div>
<Button onClick={handleGenerate} disabled={loading || !serverUrl.trim()}>
{loading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Generating...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Generate Installer
</>
)}
</Button>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && !error && (
<Alert>
<AlertDescription>
Installer generated and downloaded as forge-agent.msi.
</AlertDescription>
</Alert>
)}
<Alert>
<Shield className="h-4 w-4" />
<AlertDescription>
The installer embeds a single-use enrollment token. Each generated
MSI can only enroll one device; generate a new installer for every
device you deploy.
</AlertDescription>
</Alert>
</CardContent>
</Card>
);
}
export default function DownloadsPage() {
const [agentInfo, setAgentInfo] = useState<AgentInfo | null>(null);
const [copied, setCopied] = useState<string | null>(null);
useEffect(() => {
api.get<AgentInfo>("/v1/downloads/agent").then((res) => {
if (res.success && res.data) setAgentInfo(res.data);
});
}, []);
const handleDownload = async (filename: string) => {
const token = api.getToken();
const url = `${getApiBase()}/v1/downloads/agent/${filename}`;
const res = await fetch(url, {
headers: { Authorization: `Bearer ${token}` },
});
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
};
const copyToClipboard = (text: string, key: string) => {
navigator.clipboard.writeText(text);
setCopied(key);
setTimeout(() => setCopied(null), 2000);
};
const gpoTemplate = `msiexec /i forge-agent.msi /qn /l*v install.log`;
const uninstallTemplate = `sc stop FleetForgeAgent
msiexec /x forge-agent.msi /qn`;
const linuxTemplate = `chmod +x forge-agent-linux-amd64
sudo ./forge-agent enroll -token <TOKEN> -server http://SERVER:3002
sudo ./forge-agent run`;
const systemdTemplate = `[Unit]
Description=Fleet Forge Agent
After=network.target
[Service]
ExecStart=/usr/local/bin/forge-agent run
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target`;
return (
<DashboardLayout>
<div className="mb-8">
<h1 className="text-2xl font-bold tracking-tight">Agent Downloads</h1>
<p className="text-muted-foreground">
Download the Fleet Forge agent for your platform
</p>
</div>
<Tabs defaultValue="windows-msi" className="space-y-6">
<TabsList>
<TabsTrigger value="windows-msi">Windows MSI Installer</TabsTrigger>
<TabsTrigger value="binaries">All Binaries</TabsTrigger>
<TabsTrigger value="deployment-guide">Deployment Guide</TabsTrigger>
</TabsList>
<TabsContent value="windows-msi">
<WindowsInstallerCard />
</TabsContent>
<TabsContent value="binaries">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Download className="h-5 w-5" />
Forge Agent {agentInfo?.version}
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{agentInfo?.platforms.map((platform) => {
const Icon = osIcons[platform.os] || Download;
return (
<Card key={`${platform.os}-${platform.arch}`}>
<CardContent className="p-4 flex flex-col gap-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon className="h-5 w-5 text-muted-foreground" />
<span className="font-medium text-sm">
{platform.label}
</span>
</div>
<Badge variant="secondary" className="text-xs">
{platform.sizeFormatted}
</Badge>
</div>
<Button
size="sm"
className="w-full"
disabled={!platform.available}
onClick={() => handleDownload(platform.filename)}
>
<Download className="h-3 w-3 mr-2" />
Download
</Button>
</CardContent>
</Card>
);
})}
</div>
{!agentInfo && (
<div className="text-center text-muted-foreground py-8">
Loading agent binaries...
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="deployment-guide">
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-base">
Windows GPO Deployment
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Deploy the MSI silently across your fleet via Group Policy:
</p>
<div className="relative">
<pre className="bg-muted rounded-md p-4 text-xs overflow-x-auto font-mono whitespace-pre">
{gpoTemplate}
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(gpoTemplate, "gpo")}
>
{copied === "gpo" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Uninstall</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Stop the service and remove the agent from a Windows device:
</p>
<div className="relative">
<pre className="bg-muted rounded-md p-4 text-xs overflow-x-auto font-mono whitespace-pre">
{uninstallTemplate}
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(uninstallTemplate, "uninstall")}
>
{copied === "uninstall" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Linux Quick Start</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
Download the Linux binary, then enroll and run it:
</p>
<div className="relative">
<pre className="bg-muted rounded-md p-4 text-xs overflow-x-auto font-mono whitespace-pre">
{linuxTemplate}
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(linuxTemplate, "linux")}
>
{copied === "linux" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">
Linux Systemd Service
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
For permanent installation on Linux, create a systemd unit
file:
</p>
<div className="relative">
<pre className="bg-muted rounded-md p-4 text-xs overflow-x-auto font-mono whitespace-pre">
{systemdTemplate}
</pre>
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2"
onClick={() => copyToClipboard(systemdTemplate, "systemd")}
>
{copied === "systemd" ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</DashboardLayout>
);
}