import { useCallback, useEffect, useState, type CSSProperties } from "react"; import { Link, useSearchParams } from "react-router-dom"; import { api, type CurrentUser, type ServerRecord } from "../api/client"; import CopyButton from "../components/CopyButton"; import { formatDateTime } from "../utils/date"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { usePagination } from "../hooks/usePagination"; import Pagination from "../components/Pagination"; import { downloadCsv } from "../utils/csv"; import { TagBadges } from "../components/ServerTags"; import { AGENT_RUN_HINT, agentOsOf, installCommand, uninstallCommand, type AgentOs } from "../utils/agentCommands"; import { tagBadge, useTagColors } from "../utils/tags"; import { confirmDialog } from "../utils/dialogs"; function typeBadgeStyle(colors: Record, type: string): CSSProperties { const color = colors[type]; if (!color) return {}; return { backgroundColor: `${color}22`, color, border: `1px solid ${color}55` }; } export default function Servers({ user }: { user: CurrentUser }) { const isAdmin = user.role === "admin"; const [servers, setServers] = useState([]); const [error, setError] = useState(null); const [managingServers, setManagingServers] = useState(false); const [integrationColors, setIntegrationColors] = useState>({}); // The active tag filter lives in the URL, so a tag chip on a server's page can link straight to "everything with this tag". const [searchParams, setSearchParams] = useSearchParams(); const selectedTags = searchParams.getAll("tag"); const tagColors = useTagColors(); useEffect(() => { api.settings.integrationColors().then((res) => setIntegrationColors(res.integrationColors)).catch(() => {}); }, []); const [serverName, setServerName] = useState(""); const [serverHostname, setServerHostname] = useState(""); const [serverDescription, setServerDescription] = useState(""); const [serverOs, setServerOs] = useState("linux"); const [newToken, setNewToken] = useState<{ server: ServerRecord; token: string } | null>(null); const [uninstallFor, setUninstallFor] = useState(null); const [insecureAgent, setInsecureAgent] = useState(false); const loadServers = useCallback(() => { return api.servers .list() .then((res) => setServers(res.servers)) .catch((err) => setError(String(err))); }, []); useEffect(() => { loadServers(); }, [loadServers]); async function createServer(e: React.FormEvent) { e.preventDefault(); setError(null); try { const result = await api.servers.create({ name: serverName, hostname: serverHostname || undefined, description: serverDescription || undefined, osType: serverOs, }); setNewToken(result); setServerName(""); setServerHostname(""); setServerDescription(""); await loadServers(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function rotateToken(id: number) { try { const result = await api.servers.rotateToken(id); setNewToken(result); await loadServers(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } async function deleteServer(id: number) { if (!(await confirmDialog({ title: "Remove server", message: "Remove this server and all its tracked tasks?", confirmLabel: "Remove", danger: true }))) return; try { await api.servers.remove(id); await loadServers(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } } const { sorted: sortedServers, sortKey, sortDir, requestSort } = useSortable(servers); const { pageItems: serverPageItems, page: serverPage, setPage: setServerPage, pageCount: serverPageCount, totalCount: serverTotalCount } = usePagination(sortedServers); const tagCounts = new Map(); for (const s of servers) for (const t of s.tags) tagCounts.set(t, (tagCounts.get(t) ?? 0) + 1); // A tag from the URL that no server has (any more) stays listed, so it can still be switched off. for (const t of selectedTags) if (!tagCounts.has(t)) tagCounts.set(t, 0); const allTags = [...tagCounts.keys()].sort((a, b) => a.localeCompare(b, "sv")); const visibleServers = servers.filter((s) => selectedTags.every((t) => s.tags.includes(t))); function toggleTag(tag: string) { const next = selectedTags.includes(tag) ? selectedTags.filter((t) => t !== tag) : [...selectedTags, tag]; setSearchParams(next.length > 0 ? { tag: next } : {}, { replace: true }); } function exportCsv() { if (!sortedServers) return; downloadCsv( "servers.csv", ["Name", "Hostname", "Tags", "Token prefix", "Last seen"], sortedServers.map((s) => [ s.name, s.hostname ?? "", s.tags.join(" "), s.apiTokenPrefix, s.lastSeenAt ? formatDateTime(new Date(s.lastSeenAt)) : "never", ]), ); } return ( <>

Servers

{isAdmin && ( )}
{error &&
{error}
} {managingServers ? ( <>

Add a server

setServerName(e.target.value)} />
setServerHostname(e.target.value)} />
setServerDescription(e.target.value)} />
{newToken && (

Agent token for {newToken.server.name}

This token is only shown once — copy it now.

{newToken.token}

{AGENT_RUN_HINT[agentOsOf(newToken.server.osType)].install}

                    {installCommand(agentOsOf(newToken.server.osType), window.location.origin, newToken.token, insecureAgent)}
                  
{agentOsOf(newToken.server.osType) === "windows" && insecureAgent && (
This form is for Windows PowerShell 5.1, the one built into Windows. In PowerShell 7 the download step needs{" "} -SkipCertificateCheck instead.
)}
)} {uninstallFor && (

Uninstall agent from {uninstallFor.name}

{AGENT_RUN_HINT[agentOsOf(uninstallFor.osType)].uninstall}

                    {uninstallCommand(agentOsOf(uninstallFor.osType), window.location.origin, insecureAgent)}
                  
)}
label="Name" sortKeyName="name" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Hostname" sortKeyName="hostname" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Last seen" sortKeyName="lastSeenAt" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> {serverPageItems?.map((s) => ( ))} {sortedServers?.length === 0 && ( )}
Tags Token Actions
{s.name} {s.hostname ?? "—"}
{s.apiTokenPrefix}… {s.lastSeenAt ? formatDateTime(new Date(s.lastSeenAt)) : "never"}
No servers registered yet.
) : servers.length === 0 ? (
No servers registered yet. {isAdmin && ( <> {" "} . )}
) : ( <> {allTags.length > 0 && (
Tags {allTags.map((tag) => { const active = selectedTags.includes(tag); const look = tagBadge(tag, tagColors); return ( ); })} {selectedTags.length > 0 && ( )}
)} {visibleServers.length === 0 && (
No server has {selectedTags.length > 1 ? "all of those tags" : "that tag"}.
)}
{visibleServers.map((s) => (
{s.name}
{s.hostname ?? "no hostname reported"}
{s.proxmoxIntegrationId ? ( Proxmox ) : s.lastSeenAt ? ( Agent ) : ( No data )}
{s.tags.length > 0 && (
)}
{s.lastSeenAt ? `Last seen ${formatDateTime(new Date(s.lastSeenAt))}` : "Never reported"}
))}
)} ); }