mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
feat: move the terminal fully into the ssh-terminal plugin (B9)
This commit is contained in:
1 parent
535126fd1b
commit
79d77678c9
309 files changed
+41789
-14958
No files matched your search
@@ -0,0 +1,174 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePluginApi, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import {
|
||||
cn,
|
||||
getPollingEnvironmentMultiplier,
|
||||
runAdaptivePolling,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
|
||||
/** What the ssh-terminal plugin hands a "terminal.toolbarStatus" component. */
|
||||
interface TerminalToolbarStatusProps {
|
||||
host: { id: number | string };
|
||||
isConnected: boolean;
|
||||
/** Visible on a desktop viewport and connected: poll only while true. */
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface Sample {
|
||||
cpu: number | null;
|
||||
memory: number | null;
|
||||
disk: number | null;
|
||||
}
|
||||
|
||||
function StatBar({
|
||||
label,
|
||||
percent,
|
||||
}: {
|
||||
label: string;
|
||||
percent: number | null;
|
||||
}) {
|
||||
const value = percent == null ? null : Math.max(0, Math.min(100, percent));
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 px-1.5">
|
||||
<span className="w-12 shrink-0 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<div className="h-1.5 w-12 shrink-0 overflow-hidden rounded-full bg-muted">
|
||||
{value != null && (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-all",
|
||||
value >= 90
|
||||
? "bg-red-500"
|
||||
: value >= 70
|
||||
? "bg-yellow-500"
|
||||
: "bg-accent-brand",
|
||||
)}
|
||||
style={{ width: `${value}%` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="w-7 shrink-0 text-right text-[10px] tabular-nums text-muted-foreground">
|
||||
{value == null ? "--" : `${Math.round(value)}%`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** CPU, memory and disk bars in the terminal toolbar's expanded view. */
|
||||
export function TerminalMetricsStatus(props: Record<string, unknown>) {
|
||||
const { host, active } = props as unknown as TerminalToolbarStatusProps;
|
||||
const { t } = useTranslation();
|
||||
const api = usePluginApi();
|
||||
const [metrics, setMetrics] = useState<Sample | null>(null);
|
||||
const [available, setAvailable] = useState(true);
|
||||
const hostId = host?.id ? Number(host.id) : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!active || !hostId) {
|
||||
setMetrics(null);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
let stopPolling: (() => void) | undefined;
|
||||
let viewerSessionId: string | undefined;
|
||||
let previous: Sample | null = null;
|
||||
setAvailable(true);
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await api.get<{
|
||||
cpu?: { percent?: number };
|
||||
memory?: { percent?: number };
|
||||
disk?: { percent?: number };
|
||||
} | null>(`/metrics/${hostId}`, {
|
||||
validateStatus: (status: number) => status === 200 || status === 404,
|
||||
});
|
||||
const data = response.data;
|
||||
if (cancelled || !data) return;
|
||||
const next: Sample = {
|
||||
cpu: data.cpu?.percent ?? null,
|
||||
memory: data.memory?.percent ?? null,
|
||||
disk: data.disk?.percent ?? null,
|
||||
};
|
||||
const changed =
|
||||
!previous ||
|
||||
(["cpu", "memory", "disk"] as const).some((key) => {
|
||||
const before = previous?.[key];
|
||||
const now = next[key];
|
||||
return before == null || now == null || Math.abs(now - before) >= 2;
|
||||
});
|
||||
previous = next;
|
||||
setMetrics(next);
|
||||
return changed;
|
||||
} catch {
|
||||
// Keep the previous sample after a transient polling error.
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
const { data } = await api.post<{
|
||||
requires_totp?: boolean;
|
||||
viewerSessionId?: string;
|
||||
}>(`/metrics/start/${hostId}`);
|
||||
if (cancelled) {
|
||||
if (data.viewerSessionId) {
|
||||
void api
|
||||
.post(`/metrics/stop/${hostId}`, {
|
||||
viewerSessionId: data.viewerSessionId,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.requires_totp) {
|
||||
setAvailable(false);
|
||||
return;
|
||||
}
|
||||
viewerSessionId = data.viewerSessionId;
|
||||
stopPolling = runAdaptivePolling(
|
||||
poll,
|
||||
{
|
||||
minIntervalMs: 5_000,
|
||||
maxIntervalMs: 20_000,
|
||||
stablePollsPerStep: 2,
|
||||
maxRequestDutyCycle: 0.2,
|
||||
},
|
||||
{ intervalMultiplier: getPollingEnvironmentMultiplier },
|
||||
);
|
||||
} catch {
|
||||
if (!cancelled) setAvailable(false);
|
||||
}
|
||||
};
|
||||
void start();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
stopPolling?.();
|
||||
void api
|
||||
.post(`/metrics/stop/${hostId}`, { viewerSessionId })
|
||||
.catch(() => {});
|
||||
};
|
||||
}, [active, hostId, api]);
|
||||
|
||||
if (!available) {
|
||||
return (
|
||||
<span role="status" className="px-2 py-1 text-xs text-muted-foreground">
|
||||
{t("terminalStatus.unavailable")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatBar label={t("hostMetrics.cpu")} percent={metrics?.cpu ?? null} />
|
||||
<StatBar
|
||||
label={t("hostMetrics.memory")}
|
||||
percent={metrics?.memory ?? null}
|
||||
/>
|
||||
<StatBar label={t("hostMetrics.disk")} percent={metrics?.disk ?? null} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import HostMetricsApp from "./HostMetricsApp";
|
||||
import { HostStatsTab } from "./HostEditorStatsTab";
|
||||
import { metricsChartWidget } from "./MetricsChartWidget";
|
||||
import { MetricsRetentionSetting } from "./MetricsRetentionSetting";
|
||||
import { TerminalMetricsStatus } from "./TerminalMetricsStatus";
|
||||
|
||||
type SectionSetField = Parameters<typeof HostStatsTab>[0]["setField"];
|
||||
|
||||
@@ -90,6 +91,17 @@ export function activate(app: TermixApp): void {
|
||||
|
||||
app.registerSettingsComponent("retention", MetricsRetentionSetting);
|
||||
|
||||
// Live CPU, memory and disk bars in the terminal toolbar's expanded view.
|
||||
app.registerSlotContribution("terminal.toolbarStatus", {
|
||||
actionId: "host-metrics.terminalStatus",
|
||||
titleKey: "nav.hostMetrics",
|
||||
kind: "component",
|
||||
component: TerminalMetricsStatus,
|
||||
when: (context) =>
|
||||
(context.host as { statsConfig?: { metricsEnabled?: boolean } })
|
||||
?.statsConfig?.metricsEnabled !== false,
|
||||
});
|
||||
|
||||
app.registerSlotContribution("onboarding.features", {
|
||||
actionId: "host-metrics.feature",
|
||||
titleKey: "onboarding.feature_metrics",
|
||||
|
||||
Reference in new issue
Block a user