mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: finish the host-metrics plugin, move host status into core (B16)
This commit is contained in:
1 parent
14eb558b85
commit
0a3f05d99d
308 files changed
+53073
-8985
No files matched your search
@@ -29,8 +29,8 @@ import {
|
||||
updatePasswordResetAllowed,
|
||||
getSessionTimeout,
|
||||
updateSessionTimeout,
|
||||
getGlobalMonitoringSettings,
|
||||
updateGlobalMonitoringSettings,
|
||||
getStatusCheckSettings,
|
||||
updateStatusCheckSettings,
|
||||
getLogLevel,
|
||||
updateLogLevel,
|
||||
getOidcAutoProvision,
|
||||
@@ -134,7 +134,6 @@ export function AdminSettingsPanel({
|
||||
const [allowPasswordReset, setAllowPasswordReset] = useState(true);
|
||||
const [sessionTimeout, setSessionTimeout] = useState("24");
|
||||
const [statusInterval, setStatusInterval] = useState("60");
|
||||
const [metricsInterval, setMetricsInterval] = useState("30");
|
||||
const [logLevel, setLogLevel] = useState("info");
|
||||
const [analyticsEnabled, setAnalyticsEnabled] = useState(true);
|
||||
const [analyticsLocked, setAnalyticsLocked] = useState(false);
|
||||
@@ -359,7 +358,7 @@ export function AdminSettingsPanel({
|
||||
getPasswordLoginAllowed(),
|
||||
getPasswordResetAllowed(),
|
||||
getSessionTimeout(),
|
||||
getGlobalMonitoringSettings(),
|
||||
getStatusCheckSettings(),
|
||||
getLogLevel(),
|
||||
getOidcAutoProvision(),
|
||||
getSecondFactorAfterExternalLogin(),
|
||||
@@ -392,7 +391,6 @@ export function AdminSettingsPanel({
|
||||
setSessionTimeout(String(timeout.value.timeoutHours));
|
||||
if (monitoring.status === "fulfilled") {
|
||||
setStatusInterval(String(monitoring.value.statusCheckInterval));
|
||||
setMetricsInterval(String(monitoring.value.metricsInterval));
|
||||
}
|
||||
|
||||
if (level.status === "fulfilled") setLogLevel(level.value.level);
|
||||
@@ -630,16 +628,12 @@ export function AdminSettingsPanel({
|
||||
|
||||
async function handleSaveMonitoring() {
|
||||
const status = parseInt(statusInterval, 10);
|
||||
const metrics = parseInt(metricsInterval, 10);
|
||||
if (isNaN(status) || isNaN(metrics)) {
|
||||
if (isNaN(status) || status < 5 || status > 3600) {
|
||||
toast.error(t("admin.monitoringIntervalInvalid"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateGlobalMonitoringSettings({
|
||||
statusCheckInterval: status,
|
||||
metricsInterval: metrics,
|
||||
});
|
||||
await updateStatusCheckSettings({ statusCheckInterval: status });
|
||||
toast.success(t("admin.monitoringSaved"));
|
||||
} catch {
|
||||
toast.error(t("admin.monitoringSaveFailed"));
|
||||
@@ -1070,8 +1064,6 @@ export function AdminSettingsPanel({
|
||||
handleSaveSessionTimeout={handleSaveSessionTimeout}
|
||||
statusInterval={statusInterval}
|
||||
setStatusInterval={setStatusInterval}
|
||||
metricsInterval={metricsInterval}
|
||||
setMetricsInterval={setMetricsInterval}
|
||||
handleSaveMonitoring={handleSaveMonitoring}
|
||||
logLevel={logLevel}
|
||||
handleSaveLogLevel={handleSaveLogLevel}
|
||||
|
||||
@@ -57,8 +57,6 @@ type GeneralSettingsSectionProps = {
|
||||
handleSaveSessionTimeout: () => void;
|
||||
statusInterval: string;
|
||||
setStatusInterval: Dispatch<SetStateAction<string>>;
|
||||
metricsInterval: string;
|
||||
setMetricsInterval: Dispatch<SetStateAction<string>>;
|
||||
handleSaveMonitoring: () => void;
|
||||
logLevel: string;
|
||||
handleSaveLogLevel: (level: string) => void;
|
||||
@@ -98,8 +96,6 @@ export function AdminGeneralSettingsSection({
|
||||
handleSaveSessionTimeout,
|
||||
statusInterval,
|
||||
setStatusInterval,
|
||||
metricsInterval,
|
||||
setMetricsInterval,
|
||||
handleSaveMonitoring,
|
||||
logLevel,
|
||||
handleSaveLogLevel,
|
||||
@@ -352,22 +348,6 @@ export function AdminGeneralSettingsSection({
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("admin.sec")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-[10px] font-semibold text-muted-foreground uppercase tracking-widest">
|
||||
{t("admin.metrics")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={metricsInterval}
|
||||
onChange={(e) => setMetricsInterval(e.target.value)}
|
||||
className="w-20 text-sm"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("admin.sec")}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -972,22 +952,8 @@ export function AdminHostDefaultsSection({
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
|
||||
{t("admin.hostDefaultsMetrics")}
|
||||
{t("admin.hostDefaultsStatus")}
|
||||
</span>
|
||||
<SettingRow
|
||||
label={t("admin.hostDefaultsMetricsEnabled")}
|
||||
description={t("admin.hostDefaultsMetricsEnabledDesc")}
|
||||
>
|
||||
<AdminToggle
|
||||
on={defaults.metricsEnabled ?? true}
|
||||
onToggle={() =>
|
||||
setDefaults((p) => ({
|
||||
...p,
|
||||
metricsEnabled: !(p.metricsEnabled ?? true),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
label={t("admin.hostDefaultsStatusCheckEnabled")}
|
||||
description={t("admin.hostDefaultsStatusCheckEnabledDesc")}
|
||||
|
||||
@@ -282,29 +282,9 @@ export function createHostEditorForm(
|
||||
telnetAuthType: (host?.telnetAuthType ??
|
||||
(host?.telnetCredentialId ? "credential" : "direct")) as
|
||||
"direct" | "credential",
|
||||
statsConfig: host?.statsConfig ?? {
|
||||
statusCheckEnabled: d?.statusCheckEnabled ?? true,
|
||||
statusCheckInterval: 60,
|
||||
useGlobalStatusInterval: true,
|
||||
metricsEnabled: d?.metricsEnabled ?? true,
|
||||
metricsInterval: 30,
|
||||
useGlobalMetricsInterval: true,
|
||||
enabledWidgets: [
|
||||
"cpu",
|
||||
"memory",
|
||||
"disk",
|
||||
"network",
|
||||
"uptime",
|
||||
"system",
|
||||
"login_stats",
|
||||
"processes",
|
||||
"ports",
|
||||
"firewall",
|
||||
"temperature",
|
||||
],
|
||||
excludedMounts: [] as string[],
|
||||
monitoredMounts: [] as Array<{ path: string; label?: string }>,
|
||||
},
|
||||
statusCheckEnabled:
|
||||
host?.statusCheckEnabled ?? d?.statusCheckEnabled ?? true,
|
||||
statusCheckInterval: (host?.statusCheckInterval ?? null) as number | null,
|
||||
|
||||
// Host-scope plugin settings, keyed by plugin id. Loaded with the host and
|
||||
// saved through the plugin's own scoped route, not in the host payload.
|
||||
@@ -546,7 +526,8 @@ export function buildHostEditorPayload(
|
||||
name: a.name,
|
||||
snippetId: Number(a.snippetId),
|
||||
})),
|
||||
statsConfig: form.statsConfig,
|
||||
statusCheckEnabled: form.statusCheckEnabled,
|
||||
statusCheckInterval: form.statusCheckInterval,
|
||||
terminalConfig: protocols.enableSsh ? terminalOverrides : null,
|
||||
};
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { PasswordInput } from "@/components/password-input";
|
||||
import { FakeSwitch, SectionCard, SettingRow } from "@/components/section-card";
|
||||
import type { Host } from "@/types/ui-types";
|
||||
import {
|
||||
Activity,
|
||||
Globe,
|
||||
LayoutGrid,
|
||||
Plus,
|
||||
@@ -843,6 +844,53 @@ export function HostEditorGeneralTab({
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
title={t("hosts.statusChecksLabel")}
|
||||
icon={<Activity className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-0 py-1">
|
||||
<SettingRow
|
||||
label={t("hosts.enableStatusChecks")}
|
||||
description={t("hosts.enableStatusChecksDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.statusCheckEnabled}
|
||||
onChange={(value) => setField("statusCheckEnabled", value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
{form.statusCheckEnabled && (
|
||||
<SettingRow
|
||||
label={t("hosts.useGlobalInterval")}
|
||||
description={t("hosts.useGlobalIntervalDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.statusCheckInterval === null}
|
||||
onChange={(useGlobal) =>
|
||||
setField("statusCheckInterval", useGlobal ? null : 60)
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
)}
|
||||
{form.statusCheckEnabled && form.statusCheckInterval !== null && (
|
||||
<SettingRow
|
||||
label={t("hosts.checkIntervalS")}
|
||||
description={t("hosts.checkIntervalDesc")}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
min={5}
|
||||
max={86400}
|
||||
value={form.statusCheckInterval}
|
||||
onChange={(e) =>
|
||||
setField("statusCheckInterval", Number(e.target.value))
|
||||
}
|
||||
className="w-20 h-7 text-xs text-right [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||
/>
|
||||
</SettingRow>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
title={t("hosts.connectionToolbar")}
|
||||
icon={<LayoutGrid className="size-3.5" />}
|
||||
|
||||
@@ -150,7 +150,8 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
terminalConfig: parsedTerminalConfig as Host["terminalConfig"],
|
||||
hasSudoPassword:
|
||||
!!host.hasSudoPassword || !!parsedTerminalConfig?.sudoPassword,
|
||||
statsConfig: parseJson(h.statsConfig) as Host["statsConfig"],
|
||||
statusCheckEnabled: h.statusCheckEnabled !== false,
|
||||
statusCheckInterval: h.statusCheckInterval ?? null,
|
||||
forceKeyboardInteractive: h.forceKeyboardInteractive ?? false,
|
||||
useSocks5: h.useSocks5,
|
||||
socks5Host: h.socks5Host,
|
||||
|
||||
@@ -57,7 +57,8 @@ export const FIELD_GROUP_KEYS: Record<FieldGroup, string[]> = {
|
||||
"forceKeyboardInteractive",
|
||||
],
|
||||
advanced: [
|
||||
"statsConfig",
|
||||
"statusCheckEnabled",
|
||||
"statusCheckInterval",
|
||||
"dockerConfig",
|
||||
"webUiConfig",
|
||||
"proxmoxConfig",
|
||||
|
||||
@@ -17,7 +17,7 @@ function makeHost(overrides: Partial<Host> = {}): Host {
|
||||
enableRdp: false,
|
||||
enableVnc: false,
|
||||
enableTelnet: false,
|
||||
statsConfig: { statusCheckEnabled: true },
|
||||
statusCheckEnabled: true,
|
||||
...overrides,
|
||||
} as unknown as Host;
|
||||
}
|
||||
@@ -56,15 +56,7 @@ describe("buildStatusTooltip", () => {
|
||||
});
|
||||
|
||||
it("returns 'Monitoring disabled' when status check is disabled", () => {
|
||||
const host = makeHost({
|
||||
statsConfig: {
|
||||
enabledWidgets: [],
|
||||
statusCheckEnabled: false,
|
||||
statusCheckInterval: 30,
|
||||
metricsEnabled: false,
|
||||
metricsInterval: 30,
|
||||
},
|
||||
});
|
||||
const host = makeHost({ statusCheckEnabled: false });
|
||||
const tooltip = buildStatusTooltip(host, "online", t);
|
||||
expect(tooltip).toBe("Monitoring disabled");
|
||||
});
|
||||
@@ -106,20 +98,12 @@ describe("buildStatusTooltip", () => {
|
||||
|
||||
describe("statusCheckEnabled", () => {
|
||||
it("returns true when statusCheckEnabled is not set (default)", () => {
|
||||
const host = makeHost({ statsConfig: undefined });
|
||||
const host = makeHost({ statusCheckEnabled: undefined });
|
||||
expect(statusCheckEnabled(host)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when statusCheckEnabled is explicitly false", () => {
|
||||
const host = makeHost({
|
||||
statsConfig: {
|
||||
enabledWidgets: [],
|
||||
statusCheckEnabled: false,
|
||||
statusCheckInterval: 30,
|
||||
metricsEnabled: false,
|
||||
metricsInterval: 30,
|
||||
},
|
||||
});
|
||||
const host = makeHost({ statusCheckEnabled: false });
|
||||
expect(statusCheckEnabled(host)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -89,7 +89,7 @@ import { shell } from "@/plugin-host/shell-bridge";
|
||||
import type { TabShellCallbacks } from "@/shell/tab-registry";
|
||||
|
||||
export function statusCheckEnabled(host: Host): boolean {
|
||||
return host.statsConfig?.statusCheckEnabled !== false;
|
||||
return host.statusCheckEnabled !== false;
|
||||
}
|
||||
|
||||
export function buildStatusTooltip(
|
||||
|
||||
@@ -642,7 +642,8 @@ export function SidebarTree({
|
||||
name: a.name,
|
||||
snippetId: Number(a.snippetId),
|
||||
})),
|
||||
statsConfig: host.statsConfig,
|
||||
statusCheckEnabled: host.statusCheckEnabled,
|
||||
statusCheckInterval: host.statusCheckInterval,
|
||||
terminalConfig: host.terminalConfig ?? null,
|
||||
};
|
||||
const created = await createSSHHost(duplicateHost);
|
||||
|
||||
Reference in new issue
Block a user