import { useTranslation } from "react-i18next"; import { ExternalLink, KeyRound, Plus, Server, Settings, User, } from "lucide-react"; import { ComponentSlot } from "@/shell/ActionSlot"; import { VersionBadge } from "@/components/version-badge"; import { Facts } from "@/components/panel-layout"; import { enabledHostProtocols, protocolPort } from "@/sidebar/host-protocols"; import { activityTarget } from "@/lib/activity-types"; import { defaultConnectAction, hostActionsFor, listHostActions, } from "@/sidebar/host-contributions"; import { getStatusClasses, useStatusColorScheme, } from "@/hooks/use-status-color-scheme"; import { getDefaultConnectionTab } from "@/lib/host-connection-tabs"; import type { RecentActivityItem } from "@/main-axios"; import type { Host, TabType } from "@/types/ui-types"; import { quarterBorders } from "./quarter-borders"; export type VersionStatus = "up_to_date" | "requires_update" | "beta" | "unknown"; function Stat({ label, aside, children, }: { label: string; /** Sits beside the label, like the release channel. */ aside?: React.ReactNode; children: React.ReactNode; }) { return (
{children}
{label} {aside}
); } export function StatsStrip({ hosts, uptimeFormatted, versionText, versionStatus, releaseUrl, dbHealth, }: { hosts: Host[]; uptimeFormatted: string; versionText: string; versionStatus: VersionStatus; releaseUrl: string; dbHealth: "healthy" | "error"; }) { const { t } = useTranslation(); const online = hosts.filter((h) => h.status === "online").length; const big = "text-2xl font-bold leading-none tracking-tight"; return (
} > {versionText || "-"} {uptimeFormatted || "-"} {dbHealth === "healthy" ? t("dashboard.healthy") : t("dashboard.error")}
{online} /{hosts.length}
); } export function CountersStrip({ hosts, credentialCount, onOpenSingletonTab, }: { hosts: Host[]; credentialCount: number; onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; }) { const { t } = useTranslation(); const item = "flex items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-muted/50"; return (
); } function ActionButton({ icon: Icon, label, hint, onClick, className, }: { icon: React.ElementType; label: string; hint: string; onClick: () => void; className?: string; }) { return ( ); } export function QuickActions({ onOpenSingletonTab, hosts, onOpenTab, isAdmin, }: { onOpenSingletonTab: (type: TabType, pendingEvent?: string) => void; hosts: Host[]; onOpenTab: (host: Host, type: TabType) => void; isAdmin: boolean; }) { const { t } = useTranslation(); const pinnedHosts = hosts.filter((h) => h.pin).slice(0, 4); const endpoint = (host: Host) => { const protocol = host.enableSsh ? undefined : enabledHostProtocols(host)[0]; const port = host.enableSsh ? host.sshPort : protocol ? protocolPort(host.pluginSettings, protocol) : host.port; return `${host.ip}:${port ?? host.port}`; }; const actions = [ { icon: Plus, label: t("dashboard.addHost"), hint: t("dashboardTab.registerNewServer"), onClick: () => onOpenSingletonTab("host-manager", "host-manager:add-host"), }, { icon: KeyRound, label: t("dashboard.addCredential"), hint: t("dashboardTab.storeSshKeysOrPasswords"), onClick: () => onOpenSingletonTab("host-manager", "host-manager:add-credential"), }, ...(isAdmin ? [ { icon: Settings, label: t("dashboard.adminSettings"), hint: t("dashboardTab.manageUsersAndRoles"), onClick: () => onOpenSingletonTab("admin-settings"), }, ] : []), { icon: User, label: t("dashboard.userProfile"), hint: t("dashboardTab.manageYourAccount"), onClick: () => onOpenSingletonTab("user-profile"), }, ]; return (
{actions.map((action, i) => ( ))}
{pinnedHosts.length > 0 && (
{pinnedHosts.map((host) => { const Icon = defaultConnectAction(listHostActions(), host)?.icon ?? Server; return ( ); })}
)}
); } export function HostStatusList({ hosts, onOpenTab, statusLoading, }: { hosts: Host[]; onOpenTab: (host: Host, type: TabType) => void; statusLoading?: boolean; }) { const { t } = useTranslation(); const scheme = useStatusColorScheme(); if (hosts.length === 0) { return (
{t("dashboardTab.noHostsConfigured")}
); } return (
{hosts.map((host) => { const availability = host.status ?? "offline"; return (
{ // An overview action (a metrics view) wins over connecting. const actions = hostActionsFor(listHostActions(), host); const target = actions.find((action) => action.overview)?.tabType ?? defaultConnectAction(actions, host)?.tabType; if (target) onOpenTab(host, target); }} className="group/row flex min-w-0 cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-muted/50" > {host.name} {host.ip} {/* Plugins add live details to a host row here. */} {statusLoading || availability === "unknown" ? t("dashboardTab.checking") : availability === "online" ? t("hosts.status.online") : t("hosts.status.offline")}
); })}
); } export function RecentActivityList({ activity, hosts, onOpenTab, statusLoading, }: { activity: RecentActivityItem[]; hosts: Host[]; onOpenTab: (host: Host, type: TabType) => void; statusLoading?: boolean; }) { const { t } = useTranslation(); const scheme = useStatusColorScheme(); const ago = (ts: string) => { const diff = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); if (diff < 60) return t("dashboard.justNow"); if (diff < 3600) return `${Math.floor(diff / 60)}m`; if (diff < 86400) return `${Math.floor(diff / 3600)}h`; return `${Math.floor(diff / 86400)}d`; }; if (activity.length === 0) { return (
{t("dashboard.noRecentActivity")}
); } return (
{activity.map((item) => { const host = hosts.find((h) => h.id === String(item.hostId)); const target = activityTarget(item.type); const Icon = target?.icon ?? Server; return (
{ if (host && target) onOpenTab(host, target.tab); }} className="flex cursor-pointer items-center gap-2 px-3 py-1.5 hover:bg-muted/50" > {item.hostName} {target?.labelKey ? t(target.labelKey) : item.type.replace("_", " ")} {ago(item.timestamp)}
); })}
); }