mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
fix: finish 2.9.0 plugin moves and get every check green
This commit is contained in:
1 parent
8c1e02af89
commit
6489b62ece
335 files changed
+29878
-17749
No files matched your search
@@ -98,7 +98,12 @@ export async function collectMetrics(
|
||||
onAuthenticated?.();
|
||||
const platform = await detectHostPlatform(client);
|
||||
const result: CollectedMetrics = {
|
||||
cpu: await collectCpuMetrics(client, platform, host.id),
|
||||
cpu: await collectCpuMetrics(
|
||||
client,
|
||||
platform,
|
||||
host.id,
|
||||
state.rateSamples.cpu,
|
||||
),
|
||||
memory: await collectMemoryMetrics(client, platform),
|
||||
disk: await collectDiskMetrics(
|
||||
client,
|
||||
@@ -106,7 +111,12 @@ export async function collectMetrics(
|
||||
mounts.monitoredMounts,
|
||||
platform,
|
||||
),
|
||||
network: await collectNetworkMetrics(client, platform, host.id),
|
||||
network: await collectNetworkMetrics(
|
||||
client,
|
||||
platform,
|
||||
host.id,
|
||||
state.rateSamples.network,
|
||||
),
|
||||
uptime: await collectUptimeMetrics(client, platform),
|
||||
processes: await collectProcessesMetrics(client),
|
||||
system: await collectSystemMetrics(client, platform),
|
||||
|
||||
@@ -30,3 +30,32 @@ export function hostImportNormalizer(
|
||||
});
|
||||
return { ...settings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered as "host-metrics.hostPayloadLegacy": the 2.8 statsConfig shape
|
||||
* Termix-Mobile still reads. Remove once the mobile app reads
|
||||
* pluginSettings["host-metrics"] and GET /host/status.
|
||||
*/
|
||||
export function hostPayloadLegacy(
|
||||
values: Record<string, unknown>,
|
||||
host: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const settings = readHostMetricsSettings(values);
|
||||
const statusInterval =
|
||||
typeof host.statusCheckInterval === "number"
|
||||
? host.statusCheckInterval
|
||||
: null;
|
||||
return {
|
||||
statsConfig: {
|
||||
enabledWidgets: settings.enabledWidgets,
|
||||
statusCheckEnabled: host.statusCheckEnabled !== false,
|
||||
statusCheckInterval: statusInterval ?? 30,
|
||||
useGlobalStatusInterval: statusInterval === null,
|
||||
metricsEnabled: settings.metricsEnabled,
|
||||
metricsInterval: settings.metricsInterval ?? 30,
|
||||
useGlobalMetricsInterval: settings.metricsInterval == null,
|
||||
excludedMounts: settings.excludedMounts,
|
||||
monitoredMounts: settings.monitoredMounts,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { MetricsSessions } from "./sessions.js";
|
||||
import { CollectorRegistry } from "./collectors.js";
|
||||
import { MetricsPoller, TOPIC_HEALTH_CHECK } from "./poller.js";
|
||||
import { registerRoutes } from "./routes.js";
|
||||
import { hostImportNormalizer } from "./host-import.js";
|
||||
import { hostImportNormalizer, hostPayloadLegacy } from "./host-import.js";
|
||||
import { newSessionId, supportsMetrics } from "./helpers.js";
|
||||
|
||||
export type { MetricsCollectorV1 } from "./collectors.js";
|
||||
@@ -101,6 +101,7 @@ export async function activate(ctx: PluginContext) {
|
||||
"host-metrics.hostImportNormalizer",
|
||||
hostImportNormalizer,
|
||||
);
|
||||
ctx.registry.provide("host-metrics.hostPayloadLegacy", hostPayloadLegacy);
|
||||
|
||||
ctx.log.info("Host Metrics mounted at /plugin-api/host-metrics");
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
metricsConcurrencyFor,
|
||||
type MetricsState,
|
||||
} from "./state.js";
|
||||
import { clearCpuSampleCache } from "./widgets/cpu-collector.js";
|
||||
import { clearNetworkSampleCache } from "./widgets/network-collector.js";
|
||||
|
||||
export const DEFAULT_METRICS_INTERVAL = 30;
|
||||
export const DEFAULT_RETENTION_DAYS = 7;
|
||||
@@ -201,8 +199,7 @@ export class MetricsPoller {
|
||||
for (const hostId of [...this.polled.keys()]) this.stop(hostId, true);
|
||||
this.viewers.clear();
|
||||
this.viewerDetails.clear();
|
||||
clearCpuSampleCache();
|
||||
clearNetworkSampleCache();
|
||||
this.state.rateSamples.clear();
|
||||
}
|
||||
|
||||
/** The host as `userId` may reach it, cached for a few minutes. */
|
||||
@@ -274,8 +271,7 @@ export class MetricsPoller {
|
||||
if (clearData) {
|
||||
this.metrics.delete(hostId);
|
||||
this.state.hostCache.invalidate(hostId);
|
||||
clearCpuSampleCache(hostId);
|
||||
clearNetworkSampleCache(hostId);
|
||||
this.state.rateSamples.clear(hostId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { CpuSamples } from "./widgets/cpu-collector.js";
|
||||
import { createNetworkSamples } from "./widgets/network-collector.js";
|
||||
|
||||
export type HostStatus = "online" | "reachable" | "offline";
|
||||
|
||||
export class RequestQueue {
|
||||
@@ -422,8 +425,30 @@ export function canStartInitialMetrics(
|
||||
}
|
||||
|
||||
/** Everything one activation of the plugin polls with. */
|
||||
/** The last CPU and network sample per host, for rates between polls. */
|
||||
export function createRateSamples() {
|
||||
const cpu: CpuSamples = new Map();
|
||||
const network = createNetworkSamples();
|
||||
return {
|
||||
cpu,
|
||||
network,
|
||||
clear(hostId?: number) {
|
||||
if (hostId === undefined) {
|
||||
cpu.clear();
|
||||
network.counters.clear();
|
||||
network.windows.clear();
|
||||
return;
|
||||
}
|
||||
cpu.delete(hostId);
|
||||
network.counters.delete(hostId);
|
||||
network.windows.delete(hostId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createMetricsState() {
|
||||
return {
|
||||
rateSamples: createRateSamples(),
|
||||
/** SSH metrics execs are expensive; resized with the fleet. */
|
||||
metricsLimiter: new ConcurrentLimiter(5),
|
||||
/** Viewer registration is bursty; admit only two first samples at a time. */
|
||||
|
||||
@@ -9,20 +9,13 @@ import type { Client } from "ssh2";
|
||||
/**
|
||||
* CPU percent is derived from the delta against the previous poll's
|
||||
* /proc/stat sample rather than a same-poll double-read, so a poll no longer
|
||||
* has to block for a fixed settle window. Keyed by host.
|
||||
* has to block for a fixed settle window. Keyed by host, and owned by the
|
||||
* poller so it goes away with the plugin.
|
||||
*/
|
||||
const previousStat = new Map<
|
||||
export type CpuSamples = Map<
|
||||
number,
|
||||
{ total: number; idle: number; timestamp: number }
|
||||
>();
|
||||
|
||||
export function clearCpuSampleCache(hostId?: number): void {
|
||||
if (hostId === undefined) {
|
||||
previousStat.clear();
|
||||
return;
|
||||
}
|
||||
previousStat.delete(hostId);
|
||||
}
|
||||
>;
|
||||
|
||||
export function parseCpuLine(
|
||||
cpuLine: string,
|
||||
@@ -149,6 +142,7 @@ export async function collectCpuMetrics(
|
||||
client: Client,
|
||||
platform?: HostPlatform,
|
||||
hostId?: number,
|
||||
previousStat: CpuSamples = new Map(),
|
||||
): Promise<{
|
||||
percent: number | null;
|
||||
cores: number | null;
|
||||
|
||||
@@ -49,24 +49,16 @@ export function counterRate(
|
||||
* double-sample, so a poll no longer has to block for a fixed settle window.
|
||||
* Keyed by host, since counters from one host are meaningless against another.
|
||||
*/
|
||||
const previousSamples = new Map<
|
||||
number,
|
||||
{ counters: Map<string, NetworkCounters>; timestamp: number }
|
||||
>();
|
||||
export interface NetworkSamples {
|
||||
counters: Map<
|
||||
number,
|
||||
{ counters: Map<string, NetworkCounters>; timestamp: number }
|
||||
>;
|
||||
windows: Map<number, { rows: WindowsAdapterRow[]; timestamp: number }>;
|
||||
}
|
||||
|
||||
const previousWindowsSamples = new Map<
|
||||
number,
|
||||
{ rows: WindowsAdapterRow[]; timestamp: number }
|
||||
>();
|
||||
|
||||
export function clearNetworkSampleCache(hostId?: number): void {
|
||||
if (hostId === undefined) {
|
||||
previousSamples.clear();
|
||||
previousWindowsSamples.clear();
|
||||
return;
|
||||
}
|
||||
previousSamples.delete(hostId);
|
||||
previousWindowsSamples.delete(hostId);
|
||||
export function createNetworkSamples(): NetworkSamples {
|
||||
return { counters: new Map(), windows: new Map() };
|
||||
}
|
||||
|
||||
export function parseDarwinIfconfig(
|
||||
@@ -112,7 +104,8 @@ export function parseDarwinNetstat(
|
||||
|
||||
async function collectDarwinNetworkMetrics(
|
||||
client: Client,
|
||||
hostId?: number,
|
||||
hostId: number | undefined,
|
||||
samples: NetworkSamples,
|
||||
): Promise<{
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
@@ -143,7 +136,7 @@ async function collectDarwinNetworkMetrics(
|
||||
const netstatOut = await execCommand(client, "netstat -ib 2>/dev/null");
|
||||
const current = parseDarwinNetstat(netstatOut.stdout);
|
||||
const previous =
|
||||
hostId !== undefined ? previousSamples.get(hostId) : undefined;
|
||||
hostId !== undefined ? samples.counters.get(hostId) : undefined;
|
||||
const elapsedSeconds = previous
|
||||
? (readAt - previous.timestamp) / 1000
|
||||
: 0;
|
||||
@@ -166,7 +159,7 @@ async function collectDarwinNetworkMetrics(
|
||||
});
|
||||
}
|
||||
if (hostId !== undefined) {
|
||||
previousSamples.set(hostId, { counters: current, timestamp: readAt });
|
||||
samples.counters.set(hostId, { counters: current, timestamp: readAt });
|
||||
}
|
||||
} catch {
|
||||
for (const [name, data] of ifMap.entries()) {
|
||||
@@ -225,7 +218,8 @@ export function parseWindowsAdapterJson(output: string): WindowsAdapterRow[] {
|
||||
|
||||
async function collectWindowsNetworkMetrics(
|
||||
client: Client,
|
||||
hostId?: number,
|
||||
hostId: number | undefined,
|
||||
samples: NetworkSamples,
|
||||
): Promise<{
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
@@ -252,7 +246,7 @@ async function collectWindowsNetworkMetrics(
|
||||
const result = await execPowerShell(client, WINDOWS_ADAPTER_SCRIPT);
|
||||
const currentRows = parseWindowsAdapterJson(result.stdout);
|
||||
const previous =
|
||||
hostId !== undefined ? previousWindowsSamples.get(hostId) : undefined;
|
||||
hostId !== undefined ? samples.windows.get(hostId) : undefined;
|
||||
const elapsedSeconds = previous ? (readAt - previous.timestamp) / 1000 : 0;
|
||||
const previousMap = new Map(
|
||||
(previous?.rows ?? []).map((row) => [row.name, row]),
|
||||
@@ -276,7 +270,7 @@ async function collectWindowsNetworkMetrics(
|
||||
}
|
||||
|
||||
if (hostId !== undefined) {
|
||||
previousWindowsSamples.set(hostId, {
|
||||
samples.windows.set(hostId, {
|
||||
rows: currentRows,
|
||||
timestamp: readAt,
|
||||
});
|
||||
@@ -292,6 +286,7 @@ export async function collectNetworkMetrics(
|
||||
client: Client,
|
||||
platform?: HostPlatform,
|
||||
hostId?: number,
|
||||
samples: NetworkSamples = createNetworkSamples(),
|
||||
): Promise<{
|
||||
interfaces: Array<{
|
||||
name: string;
|
||||
@@ -304,10 +299,10 @@ export async function collectNetworkMetrics(
|
||||
}>;
|
||||
}> {
|
||||
if (platform === "darwin") {
|
||||
return collectDarwinNetworkMetrics(client, hostId);
|
||||
return collectDarwinNetworkMetrics(client, hostId, samples);
|
||||
}
|
||||
if (platform === "windows") {
|
||||
return collectWindowsNetworkMetrics(client, hostId);
|
||||
return collectWindowsNetworkMetrics(client, hostId, samples);
|
||||
}
|
||||
|
||||
const interfaces: Array<{
|
||||
@@ -368,7 +363,7 @@ export async function collectNetworkMetrics(
|
||||
const procNet = await execCommand(client, "cat /proc/net/dev");
|
||||
const rxTxMap = parseNetworkCounters(procNet.stdout);
|
||||
const previous =
|
||||
hostId !== undefined ? previousSamples.get(hostId) : undefined;
|
||||
hostId !== undefined ? samples.counters.get(hostId) : undefined;
|
||||
const elapsedSeconds = previous
|
||||
? (readAt - previous.timestamp) / 1000
|
||||
: 0;
|
||||
@@ -395,7 +390,7 @@ export async function collectNetworkMetrics(
|
||||
});
|
||||
}
|
||||
if (hostId !== undefined) {
|
||||
previousSamples.set(hostId, {
|
||||
samples.counters.set(hostId, {
|
||||
counters: rxTxMap,
|
||||
timestamp: readAt,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Separator,
|
||||
Button,
|
||||
useAreaPreferences,
|
||||
useConfirmation,
|
||||
TOTPDialog,
|
||||
useTabsSafe,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
CardGridCanvas,
|
||||
ColumnCountStepper,
|
||||
type GridCardCatalogEntry,
|
||||
logActivity,
|
||||
SnippetVariablesDialog,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { readHostMetricsSettings } from "../shared/stats-widgets.js";
|
||||
@@ -52,6 +50,8 @@ import {
|
||||
useHost,
|
||||
useHostStatus,
|
||||
invokeAction,
|
||||
logActivity,
|
||||
usePluginUiPreferences,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { RefreshCw, Server, LayoutDashboard } from "lucide-react";
|
||||
@@ -161,7 +161,9 @@ function HostMetricsInner({
|
||||
const hostId = currentHostConfig?.id ?? null;
|
||||
const { layout, setLayout } = useHostMetricsPreferences(hostId);
|
||||
|
||||
const metricsPrefs = useAreaPreferences("hostMetrics");
|
||||
const { values: metricsPrefs } = usePluginUiPreferences<{
|
||||
columns: number;
|
||||
}>();
|
||||
|
||||
const effectiveLayout: HostMetricsLayout = React.useMemo(() => {
|
||||
// A saved layout is user-authored, so the preset never rewrites it -- it
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
usePluginApi,
|
||||
usePluginApiFor,
|
||||
useTranslation,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
cn,
|
||||
getPollingEnvironmentMultiplier,
|
||||
resolveConnectionOrigin,
|
||||
resolveRemoteHostId,
|
||||
runAdaptivePolling,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
|
||||
/** What the ssh-terminal plugin hands a "terminal.toolbarStatus" component. */
|
||||
interface TerminalToolbarStatusProps {
|
||||
host: { id: number | string };
|
||||
host: {
|
||||
id: number | string;
|
||||
connectionOrigin?: "local" | "remote" | null;
|
||||
syncId?: string | null;
|
||||
};
|
||||
isConnected: boolean;
|
||||
/** Visible on a desktop viewport and connected: poll only while true. */
|
||||
active: boolean;
|
||||
@@ -59,7 +69,8 @@ function StatBar({
|
||||
export function TerminalMetricsStatus(props: Record<string, unknown>) {
|
||||
const { host, active } = props as unknown as TerminalToolbarStatusProps;
|
||||
const { t } = useTranslation();
|
||||
const api = usePluginApi();
|
||||
const localApi = usePluginApi();
|
||||
const remoteApi = usePluginApiFor("remote");
|
||||
const [metrics, setMetrics] = useState<Sample | null>(null);
|
||||
const [available, setAvailable] = useState(true);
|
||||
const hostId = host?.id ? Number(host.id) : null;
|
||||
@@ -73,6 +84,9 @@ export function TerminalMetricsStatus(props: Record<string, unknown>) {
|
||||
let stopPolling: (() => void) | undefined;
|
||||
let viewerSessionId: string | undefined;
|
||||
let previous: Sample | null = null;
|
||||
// A host on the desktop app's remote server has its own id there.
|
||||
let api = localApi;
|
||||
let targetId: number = hostId;
|
||||
setAvailable(true);
|
||||
|
||||
const poll = async () => {
|
||||
@@ -81,7 +95,7 @@ export function TerminalMetricsStatus(props: Record<string, unknown>) {
|
||||
cpu?: { percent?: number };
|
||||
memory?: { percent?: number };
|
||||
disk?: { percent?: number };
|
||||
} | null>(`/metrics/${hostId}`, {
|
||||
} | null>(`/metrics/${targetId}`, {
|
||||
validateStatus: (status: number) => status === 200 || status === 404,
|
||||
});
|
||||
const data = response.data;
|
||||
@@ -109,14 +123,24 @@ export function TerminalMetricsStatus(props: Record<string, unknown>) {
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
if ((await resolveConnectionOrigin(host)) === "remote") {
|
||||
const remoteId = await resolveRemoteHostId(host.syncId);
|
||||
if (cancelled) return;
|
||||
if (remoteId === null) {
|
||||
setAvailable(false);
|
||||
return;
|
||||
}
|
||||
api = remoteApi;
|
||||
targetId = remoteId;
|
||||
}
|
||||
const { data } = await api.post<{
|
||||
requires_totp?: boolean;
|
||||
viewerSessionId?: string;
|
||||
}>(`/metrics/start/${hostId}`);
|
||||
}>(`/metrics/start/${targetId}`);
|
||||
if (cancelled) {
|
||||
if (data.viewerSessionId) {
|
||||
void api
|
||||
.post(`/metrics/stop/${hostId}`, {
|
||||
.post(`/metrics/stop/${targetId}`, {
|
||||
viewerSessionId: data.viewerSessionId,
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -148,10 +172,10 @@ export function TerminalMetricsStatus(props: Record<string, unknown>) {
|
||||
cancelled = true;
|
||||
stopPolling?.();
|
||||
void api
|
||||
.post(`/metrics/stop/${hostId}`, { viewerSessionId })
|
||||
.post(`/metrics/stop/${targetId}`, { viewerSessionId })
|
||||
.catch(() => {});
|
||||
};
|
||||
}, [active, hostId, api]);
|
||||
}, [active, hostId, localApi, remoteApi]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
if (!available) {
|
||||
return (
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { useManagerData, extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerSearch } from "./ManagerToolbar";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
import { ManagerSearch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface CronEntry {
|
||||
raw: string;
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { useManagerData, extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerSearch } from "./ManagerToolbar";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
import { ManagerSearch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface FirewallData {
|
||||
type: "iptables" | "nftables" | "none";
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { useManagerData, extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface HealthCheck {
|
||||
id: string;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ScrollText } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface LogFiles {
|
||||
common: string[];
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Button, MetricCard } from "@termix/plugin-sdk/ui";
|
||||
import type { ReactNode } from "react";
|
||||
import { RefreshCw, AlertTriangle } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { ManagerError } from "./useManagerData";
|
||||
|
||||
export function ManagerCardShell({
|
||||
title,
|
||||
icon,
|
||||
loading,
|
||||
error,
|
||||
onRefresh,
|
||||
empty,
|
||||
emptyMessage,
|
||||
children,
|
||||
headerExtra,
|
||||
}: {
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
loading: boolean;
|
||||
error: ManagerError | null;
|
||||
onRefresh: () => void;
|
||||
empty?: boolean;
|
||||
emptyMessage?: string;
|
||||
children: ReactNode;
|
||||
headerExtra?: ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<MetricCard
|
||||
title={title}
|
||||
icon={icon}
|
||||
scroll
|
||||
action={
|
||||
<div className="flex items-center gap-1">
|
||||
{headerExtra}
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="flex size-6 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
|
||||
title={t("hostMetrics.refresh")}
|
||||
>
|
||||
<RefreshCw className={`size-3 ${loading ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{error ? (
|
||||
<div className="flex flex-col items-center gap-2 py-6 text-center">
|
||||
<AlertTriangle className="size-6 text-yellow-500" />
|
||||
<span className="text-xs text-muted-foreground">{error.message}</span>
|
||||
{error.code === "SUDO_REQUIRED" && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{t("hostMetrics.managers.sudoHint")}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRefresh}
|
||||
className="mt-1"
|
||||
>
|
||||
{t("hostMetrics.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
) : empty ? (
|
||||
<div className="flex items-center justify-center py-8 text-xs text-muted-foreground/50">
|
||||
{emptyMessage ?? t("hostMetrics.managers.noData")}
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</MetricCard>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
|
||||
/**
|
||||
* Compact search row shared by manager cards. Optional `extra` renders to the
|
||||
* right (filters, action buttons) and `count` shows a result tally.
|
||||
*/
|
||||
export function ManagerSearch({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
count,
|
||||
extra,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
count?: number;
|
||||
extra?: ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="mb-2 flex items-center gap-1.5">
|
||||
<div className="relative flex-1">
|
||||
<Search className="pointer-events-none absolute left-2 top-1/2 size-3 -translate-y-1/2 text-muted-foreground/60" />
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder ?? t("hostMetrics.managers.filter")}
|
||||
className="h-7 w-full border border-border bg-background pl-7 pr-2 text-xs outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
</div>
|
||||
{count != null && (
|
||||
<span className="shrink-0 text-[10px] tabular-nums text-muted-foreground">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
{extra}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { useManagerData, extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerSearch } from "./ManagerToolbar";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
import { ManagerSearch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface UpgradablePackage {
|
||||
name: string;
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { useManagerData, extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerSearch } from "./ManagerToolbar";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
import { ManagerSearch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface ProcessRow {
|
||||
pid: number;
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { useManagerData, extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerSearch } from "./ManagerToolbar";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
import { ManagerSearch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface SystemdService {
|
||||
unit: string;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useMemo, useState } from "react";
|
||||
import { MemoryStick, Timer, HardDrive } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { useManagerData } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerSearch } from "./ManagerToolbar";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
import { ManagerSearch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface MemProc {
|
||||
pid: number;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useManagerAction,
|
||||
extractError,
|
||||
} from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface CertInfo {
|
||||
client: string;
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { toast } from "sonner";
|
||||
import { useHostMetricsApi } from "../../host-metrics-api";
|
||||
import { useManagerData, extractError } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerSearch } from "./ManagerToolbar";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
import { ManagerSearch } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface SystemUser {
|
||||
name: string;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { useManagerData, useManagerAction } from "./useManagerData";
|
||||
import { ManagerCardShell } from "./ManagerCardShell";
|
||||
import { ManagerCardShell } from "@termix/plugin-sdk/ui";
|
||||
|
||||
interface WireGuardPeer {
|
||||
publicKey: string;
|
||||
|
||||
Reference in new issue
Block a user