mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-25 08:08:39 +00:00
feat: move proxmox discovery/import/sync into a first-party plugin
This commit is contained in:
1 parent
9db66d5780
commit
d440ce9073
21 files changed
+290
-36
No files matched your search
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Proxmox discovery/import/sync - first-party, in-process plugin.
|
||||
*
|
||||
* This plugin runs on the main thread rather than in a worker for the same
|
||||
* reason ai does (see src/backend/plugins/first-party.ts): it writes
|
||||
* directly across hosts through the host repository, resolves and connects
|
||||
* to hosts over the shared SSH pool via resolveHostById and
|
||||
* createJumpHostChain, verifies host keys via SSHHostKeyVerifier, and
|
||||
* streams its discovery progress as server-sent events. None of that fits
|
||||
* the worker ctx -- ctx.hosts is a 13-field read-only view with no write
|
||||
* methods, and ctx.http.route replies with a single postMessage value
|
||||
* rather than a stream.
|
||||
*
|
||||
* Like ai, this plugin does not own a live transport (no WebSocket server,
|
||||
* no dedicated port), so it registers its Express router with the
|
||||
* always-mounted /proxmox dispatcher in
|
||||
* src/backend/database/routes/proxmox-dispatch.ts instead of getting its
|
||||
* own port.
|
||||
*
|
||||
* routes.ts is what used to be src/backend/database/routes/proxmox.ts,
|
||||
* moved here with its sibling modules (proxmox-import-auth.ts,
|
||||
* proxmox-jump-hosts.ts) and rewritten from `export default router` into
|
||||
* startProxmoxService()/stopProxmoxService(), called from
|
||||
* activate()/deactivate() below. Those two functions also start and stop
|
||||
* the background auto-sync interval that used to run at module scope in
|
||||
* proxmox.ts -- keeping it there would mean it fires even when this plugin
|
||||
* is disabled and can never be torn down.
|
||||
*/
|
||||
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* Loads a module by its path relative to this file's own directory.
|
||||
*
|
||||
* The plugin ships beside dist/backend, so in a built server the compiled
|
||||
* .js for the moved Proxmox files (routes.js, ...) sits right next to this
|
||||
* .mjs, and core modules resolve via ../../../backend/backend/... from
|
||||
* there (dist/plugins/proxmox/backend -> dist/backend/backend). Under
|
||||
* vitest/dev nothing imports this plugin directly (the loader loads
|
||||
* in-process plugins with a plain dynamic import, no tsx loader involved),
|
||||
* so the only layout that needs to resolve here is the built one.
|
||||
*/
|
||||
async function loadRelative(relativePath) {
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const target = path.join(here, `${relativePath}.js`);
|
||||
return import(new URL(`file://${target.replace(/\\/g, "/")}`).href);
|
||||
}
|
||||
|
||||
let routesModule = null;
|
||||
|
||||
export async function activate(ctx) {
|
||||
routesModule = await loadRelative("routes");
|
||||
routesModule.startProxmoxService();
|
||||
ctx.log.info("Proxmox router registered at /proxmox");
|
||||
}
|
||||
|
||||
export async function deactivate() {
|
||||
if (routesModule) {
|
||||
routesModule.stopProxmoxService();
|
||||
routesModule = null;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// Pure decision: which auth settings an imported Proxmox guest inherits.
|
||||
//
|
||||
// The frontend carries a parallel copy in
|
||||
// src/ui/components/proxmox/proxmox-import-auth.ts. The two drifting apart is
|
||||
// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is
|
||||
// what produced the reported import bug, so both are kept behaviourally
|
||||
// identical and each is unit-tested against the same matrix.
|
||||
export function resolveProxmoxImportAuth(
|
||||
File renamed without changes.
@@ -1,24 +1,28 @@
|
||||
import { getErrorMessage } from "../../utils/error-message.js";
|
||||
import { getErrorMessage } from "../../../src/backend/utils/error-message.js";
|
||||
import express from "express";
|
||||
import { Client as SSHClient } from "ssh2";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { DataCrypto } from "../../utils/data-crypto.js";
|
||||
import { createCurrentHostRepository } from "../repositories/factory.js";
|
||||
import { AuthManager } from "../../utils/auth-manager.js";
|
||||
import { logger } from "../../../src/backend/utils/logger.js";
|
||||
import { DataCrypto } from "../../../src/backend/utils/data-crypto.js";
|
||||
import { createCurrentHostRepository } from "../../../src/backend/database/repositories/factory.js";
|
||||
import { AuthManager } from "../../../src/backend/utils/auth-manager.js";
|
||||
import {
|
||||
type AuthenticatedRequest,
|
||||
type SSHHost,
|
||||
} from "../../../types/index.js";
|
||||
import { SSHHostKeyVerifier } from "../../hosts/host-key-verifier.js";
|
||||
import { resolveHostById } from "../../hosts/host-resolver.js";
|
||||
import { createJumpHostChain } from "../../hosts/jump-host-chain.js";
|
||||
} from "../../../src/types/index.js";
|
||||
import { SSHHostKeyVerifier } from "../../../src/backend/hosts/host-key-verifier.js";
|
||||
import { resolveHostById } from "../../../src/backend/hosts/host-resolver.js";
|
||||
import { createJumpHostChain } from "../../../src/backend/hosts/jump-host-chain.js";
|
||||
import { resolveProxmoxImportAuth } from "./proxmox-import-auth.js";
|
||||
import {
|
||||
parseProxmoxJumpHosts,
|
||||
serializeProxmoxJumpHosts,
|
||||
} from "./proxmox-jump-hosts.js";
|
||||
import { isSafeNodeName } from "../../hosts/proxmox-shared.js";
|
||||
import { execElevated } from "../../hosts/metrics-shared/exec-elevated.js";
|
||||
import { isSafeNodeName } from "../../../src/backend/hosts/proxmox-shared.js";
|
||||
import { execElevated } from "../../../src/backend/hosts/metrics-shared/exec-elevated.js";
|
||||
import {
|
||||
registerProxmoxRouter,
|
||||
unregisterProxmoxRouter,
|
||||
} from "../../../src/backend/database/routes/proxmox-dispatch.js";
|
||||
|
||||
const router = express.Router();
|
||||
const proxmoxLogger = logger;
|
||||
@@ -346,7 +350,7 @@ async function discoverProxmoxGuestsForHost(
|
||||
sshConfig.passphrase = resolvedCredentials.keyPassword;
|
||||
} else if (authType === "agent") {
|
||||
const { applyAgentAuth } =
|
||||
await import("../../hosts/terminal-auth-helpers.js");
|
||||
await import("../../../src/backend/hosts/terminal-auth-helpers.js");
|
||||
const result = await applyAgentAuth(
|
||||
sshConfig,
|
||||
host.terminalConfig as unknown as Record<string, unknown> | undefined,
|
||||
@@ -931,13 +935,8 @@ async function runDueProxmoxAutoSyncs(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
const proxmoxAutoSyncTimer = setInterval(runDueProxmoxAutoSyncs, 60 * 1000);
|
||||
proxmoxAutoSyncTimer.unref?.();
|
||||
const proxmoxAutoSyncStartupTimer = setTimeout(
|
||||
runDueProxmoxAutoSyncs,
|
||||
30 * 1000,
|
||||
);
|
||||
proxmoxAutoSyncStartupTimer.unref?.();
|
||||
let proxmoxAutoSyncTimer: NodeJS.Timeout | undefined;
|
||||
let proxmoxAutoSyncStartupTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
@@ -1136,4 +1135,24 @@ router.post(
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
/** Called from activate(). Mounts this router at /proxmox via the shared
|
||||
* dispatcher, and starts the background auto-sync scan (a 60s interval plus
|
||||
* a one-off 30s-delayed startup run, both unref'd so they never keep the
|
||||
* process alive on their own). */
|
||||
export function startProxmoxService(): void {
|
||||
registerProxmoxRouter(router);
|
||||
proxmoxAutoSyncTimer = setInterval(runDueProxmoxAutoSyncs, 60 * 1000);
|
||||
proxmoxAutoSyncTimer.unref?.();
|
||||
proxmoxAutoSyncStartupTimer = setTimeout(runDueProxmoxAutoSyncs, 30 * 1000);
|
||||
proxmoxAutoSyncStartupTimer.unref?.();
|
||||
}
|
||||
|
||||
/** Called from deactivate(). /proxmox/* falls back to 404 until reactivated,
|
||||
* and the auto-sync timers are cleared so a disabled plugin stops syncing. */
|
||||
export function stopProxmoxService(): void {
|
||||
unregisterProxmoxRouter();
|
||||
if (proxmoxAutoSyncTimer) clearInterval(proxmoxAutoSyncTimer);
|
||||
if (proxmoxAutoSyncStartupTimer) clearTimeout(proxmoxAutoSyncStartupTimer);
|
||||
proxmoxAutoSyncTimer = undefined;
|
||||
proxmoxAutoSyncStartupTimer = undefined;
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Frontend half of the proxmox plugin.
|
||||
*
|
||||
* Unlike ssh-terminal/docker, proxmox has no rail tab or standalone view —
|
||||
* only a discovery dialog (opened imperatively from HostsPanel.tsx, not
|
||||
* through a tab seam) and a host-editor settings tab. Registers the
|
||||
* host-editor tab through the same registerHostEditorTab seam docker uses.
|
||||
*
|
||||
* HostProxmoxTab itself stays in src/ui/sidebar/HostEditorFeatureTabs.tsx
|
||||
* rather than moving into this plugin: it takes the same {form, setField}
|
||||
* pair every other host editor tab does, and those types (HostEditorForm,
|
||||
* the setField signature) are core types HostEditor.tsx owns, the same
|
||||
* reason ssh-terminal leaves session-manager.ts in core rather than copying
|
||||
* it here.
|
||||
*
|
||||
* register() is called when the plugin is enabled, unregister() when it is
|
||||
* disabled. As of this change neither is actually invoked anywhere yet --
|
||||
* see plugins/ssh-terminal/frontend/index.mjs and
|
||||
* src/ui/tests/sidebar/plugin-extension-seam.test.tsx, which says outright
|
||||
* "there is no plugin loader yet" for the frontend half. This file follows
|
||||
* the same shape so it is ready the moment that loader exists. The dialog
|
||||
* is reached today through a direct import in HostsPanel.tsx, not through
|
||||
* this seam.
|
||||
*/
|
||||
|
||||
export const id = "proxmox";
|
||||
export const hostEditorTabId = "proxmox";
|
||||
|
||||
export async function register({ registerHostEditorTab, icons }) {
|
||||
if (registerHostEditorTab) {
|
||||
const { HostProxmoxTab } =
|
||||
await import("../../../src/ui/sidebar/HostEditorFeatureTabs.tsx");
|
||||
registerHostEditorTab({
|
||||
id: hostEditorTabId,
|
||||
labelKey: "hosts.tabProxmox",
|
||||
icon: icons.Server,
|
||||
component: HostProxmoxTab,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregister({ unregisterHostEditorTab }) {
|
||||
unregisterHostEditorTab?.(hostEditorTabId);
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "proxmox",
|
||||
"name": "Proxmox",
|
||||
"version": "1.0.0",
|
||||
"description": "Discover Proxmox VE guests over an existing SSH host, import them as Termix hosts, and keep them in sync.",
|
||||
"author": { "name": "Termix" },
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Termix-SSH/Termix",
|
||||
"category": "Infrastructure",
|
||||
"icon": "Server",
|
||||
"engine": { "termix": ">=2.9.0", "api": "1" },
|
||||
"capabilities": {
|
||||
"backend": true,
|
||||
"frontend": true,
|
||||
"electron": true,
|
||||
"platforms": ["linux", "win32", "darwin"]
|
||||
},
|
||||
"permissions": ["process:transport-owner"],
|
||||
"contributes": {
|
||||
"hostCapability": {
|
||||
"key": "enableProxmox",
|
||||
"labelKey": "hosts.proxmoxIntegration",
|
||||
"editorTab": "proxmox"
|
||||
}
|
||||
},
|
||||
"sidecars": []
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import userPreferencesRoutes from "./routes/user-preferences.js";
|
||||
import hostSidebarPreferencesRoutes from "./routes/host-sidebar-preferences.js";
|
||||
import credentialSidebarPreferencesRoutes from "./routes/credential-sidebar-preferences.js";
|
||||
import uiPreferencesRoutes from "./routes/ui-preferences.js";
|
||||
import proxmoxRoutes from "./routes/proxmox.js";
|
||||
import proxmoxDispatch from "./routes/proxmox-dispatch.js";
|
||||
import termixIdRoutes from "./routes/termix-id.js";
|
||||
import { registerAuditLogRoutes } from "./routes/audit-log-routes.js";
|
||||
import { registerTailscaleRoutes } from "./routes/tailscale-routes.js";
|
||||
@@ -1768,7 +1768,7 @@ app.use("/user-preferences", userPreferencesRoutes);
|
||||
app.use("/host-sidebar/preferences", hostSidebarPreferencesRoutes);
|
||||
app.use("/credential-sidebar/preferences", credentialSidebarPreferencesRoutes);
|
||||
app.use("/ui-preferences", uiPreferencesRoutes);
|
||||
app.use("/proxmox", proxmoxRoutes);
|
||||
app.use("/proxmox", proxmoxDispatch);
|
||||
app.use("/termix-id", termixIdRoutes);
|
||||
registerAuditLogRoutes(app, authenticateJWT);
|
||||
registerTailscaleRoutes(app, authenticateJWT);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Mounted at /proxmox. The Proxmox discovery/import/sync feature ships as a
|
||||
// first-party in-process plugin (plugins/proxmox) rather than a route file
|
||||
// imported directly here, so this module exists to keep /proxmox/* working
|
||||
// at its original path without handing the plugin a reference to the shared
|
||||
// app instance. The plugin runtime registers its router via
|
||||
// registerProxmoxRouter when it activates, and clears it on deactivate, so a
|
||||
// disabled plugin falls back to the 404 below.
|
||||
//
|
||||
// Unlike /plugin-api, this dispatcher applies no auth of its own: it forwards
|
||||
// to exactly one first-party router (the Proxmox plugin's own), and every
|
||||
// route in that router already applies authenticateJWT/requireDataAccess
|
||||
// itself, the same as every other router mounted alongside it here (/users,
|
||||
// /host, /snippets, ...). /plugin-api gates at the mount point because it
|
||||
// forwards to arbitrary third-party plugin code that cannot be assumed to
|
||||
// gate itself; that reasoning does not apply to a single first-party router
|
||||
// that already does.
|
||||
|
||||
import type { Request, Response, NextFunction, Router } from "express";
|
||||
import { databaseLogger } from "../../utils/logger.js";
|
||||
|
||||
let activeRouter: Router | null = null;
|
||||
|
||||
export function registerProxmoxRouter(router: Router): void {
|
||||
activeRouter = router;
|
||||
databaseLogger.info("Registered Proxmox plugin router", {
|
||||
operation: "proxmox_dispatch_register",
|
||||
});
|
||||
}
|
||||
|
||||
export function unregisterProxmoxRouter(): void {
|
||||
if (!activeRouter) return;
|
||||
activeRouter = null;
|
||||
databaseLogger.info("Unregistered Proxmox plugin router", {
|
||||
operation: "proxmox_dispatch_register",
|
||||
});
|
||||
}
|
||||
|
||||
export default function proxmoxDispatch(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): void {
|
||||
if (!activeRouter) {
|
||||
res.status(404).json({ error: "Proxmox discovery is not available" });
|
||||
return;
|
||||
}
|
||||
activeRouter(req, res, next);
|
||||
}
|
||||
@@ -54,12 +54,21 @@
|
||||
* with no write methods and no snippets/fleets/alerts/automations surface at
|
||||
* all, and ctx.http.route replies with one postMessage value rather than a
|
||||
* stream, so none of that fits the worker ctx either.
|
||||
*
|
||||
* proxmox is here for the same reason as ai: it does not own a transport,
|
||||
* but it writes directly across hosts through the host repository during
|
||||
* discovery and sync, resolves and connects to hosts via resolveHostById and
|
||||
* createJumpHostChain the same way the terminal and ai's command executor
|
||||
* do, verifies host keys via SSHHostKeyVerifier, and streams its discovery
|
||||
* progress as server-sent events. Neither the worker's 13-field read-only
|
||||
* ctx.hosts nor its single-postMessage-reply ctx.http.route can support that.
|
||||
*/
|
||||
export const FIRST_PARTY_PLUGIN_IDS: ReadonlySet<string> = new Set([
|
||||
"ssh-terminal",
|
||||
"docker",
|
||||
"host-metrics",
|
||||
"ai",
|
||||
"proxmox",
|
||||
]);
|
||||
|
||||
export const TRANSPORT_OWNER_CAPABILITY = "process:transport-owner";
|
||||
|
||||
@@ -299,9 +299,11 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
|
||||
// ssh-terminal, docker and host-metrics plugins start their own servers,
|
||||
// so disabling any of them stops its WS/HTTP server. See
|
||||
// plugins/ssh-terminal, plugins/docker and plugins/host-metrics.
|
||||
// AI is also absent: the ai plugin registers its router with the /ai
|
||||
// dispatcher in database.ts on activate, so disabling it makes /ai/* 404
|
||||
// instead of leaving a dead import here. See plugins/ai.
|
||||
// AI and Proxmox are also absent for the same reason: each plugin
|
||||
// registers its router with its own dispatcher in database.ts on
|
||||
// activate (/ai, /proxmox), so disabling either makes its routes 404
|
||||
// instead of leaving a dead import here. See plugins/ai and
|
||||
// plugins/proxmox.
|
||||
await import("./hosts/tunnel/index.js");
|
||||
await import("./hosts/file-manager/index.js");
|
||||
await import("./hosts/tmux/index.js");
|
||||
|
||||
@@ -39,20 +39,22 @@ function manifest(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe("first-party allowlist", () => {
|
||||
it("contains only ssh-terminal, docker, host-metrics and ai", () => {
|
||||
it("contains only ssh-terminal, docker, host-metrics, ai and proxmox", () => {
|
||||
expect([...FIRST_PARTY_PLUGIN_IDS].sort()).toEqual([
|
||||
"ai",
|
||||
"docker",
|
||||
"host-metrics",
|
||||
"proxmox",
|
||||
"ssh-terminal",
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognises ssh-terminal, docker, host-metrics and ai and nothing else", () => {
|
||||
it("recognises ssh-terminal, docker, host-metrics, ai and proxmox and nothing else", () => {
|
||||
expect(isFirstParty("ssh-terminal")).toBe(true);
|
||||
expect(isFirstParty("docker")).toBe(true);
|
||||
expect(isFirstParty("host-metrics")).toBe(true);
|
||||
expect(isFirstParty("ai")).toBe(true);
|
||||
expect(isFirstParty("proxmox")).toBe(true);
|
||||
expect(isFirstParty("ssh-terminal-pro")).toBe(false);
|
||||
expect(isFirstParty("community-plugin")).toBe(false);
|
||||
expect(isFirstParty("")).toBe(false);
|
||||
@@ -69,6 +71,7 @@ describe("runsInProcess", () => {
|
||||
true,
|
||||
);
|
||||
expect(runsInProcess("ai", [TRANSPORT_OWNER_CAPABILITY])).toBe(true);
|
||||
expect(runsInProcess("proxmox", [TRANSPORT_OWNER_CAPABILITY])).toBe(true);
|
||||
|
||||
// On the list but not asking for it: stays in a worker, so the manifest
|
||||
// remains an honest description of what the plugin does.
|
||||
@@ -76,6 +79,7 @@ describe("runsInProcess", () => {
|
||||
expect(runsInProcess("docker", ["hosts.read"])).toBe(false);
|
||||
expect(runsInProcess("host-metrics", ["hosts.read"])).toBe(false);
|
||||
expect(runsInProcess("ai", ["hosts.read"])).toBe(false);
|
||||
expect(runsInProcess("proxmox", ["hosts.read"])).toBe(false);
|
||||
|
||||
// Asking for it but not on the list.
|
||||
expect(
|
||||
@@ -155,6 +159,18 @@ describe("manifest gate on the reserved capability", () => {
|
||||
expect(parsed?.id).toBe("ai");
|
||||
});
|
||||
|
||||
it("allows it for proxmox", () => {
|
||||
const { manifest: parsed, errors } = parseManifest(
|
||||
manifest({
|
||||
id: "proxmox",
|
||||
permissions: [TRANSPORT_OWNER_CAPABILITY],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
expect(parsed?.id).toBe("proxmox");
|
||||
});
|
||||
|
||||
it("still accepts an ordinary manifest that never mentions it", () => {
|
||||
const { manifest: parsed, errors } = parseManifest(
|
||||
manifest({ permissions: ["hosts.read"] }),
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProxmoxImportAuth } from "../../../database/routes/proxmox-import-auth.js";
|
||||
import { resolveProxmoxImportAuth } from "../../../../../plugins/proxmox/backend/proxmox-import-auth.js";
|
||||
|
||||
// The frontend carries its own copy of this decision in
|
||||
// src/ui/components/proxmox/proxmox-import-auth.ts. The two drifting apart is
|
||||
// plugins/proxmox/frontend/proxmox-import-auth.ts. The two drifting apart is
|
||||
// what produced the reported bug, so both are held to the same matrix.
|
||||
describe("resolveProxmoxImportAuth", () => {
|
||||
it("uses the default credential for key auth when one is configured", () => {
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
parseProxmoxJumpHosts,
|
||||
serializeProxmoxJumpHosts,
|
||||
} from "../../../database/routes/proxmox-jump-hosts.js";
|
||||
} from "../../../../../plugins/proxmox/backend/proxmox-jump-hosts.js";
|
||||
|
||||
describe("Proxmox jump-host persistence", () => {
|
||||
const jumpHosts = [{ hostId: 7 }, { hostId: 9 }];
|
||||
+2
-1
@@ -6,7 +6,8 @@ vi.mock("../../../hosts/metrics-shared/exec-elevated.js", () => ({
|
||||
execElevated: (...args: unknown[]) => execElevated(...args),
|
||||
}));
|
||||
|
||||
import { execPveshCommand } from "../../../database/routes/proxmox.js";
|
||||
const { execPveshCommand } =
|
||||
await import("../../../../../plugins/proxmox/backend/routes.js");
|
||||
|
||||
const client = {} as Client;
|
||||
|
||||
@@ -84,6 +84,7 @@ import {
|
||||
HostDockerTab,
|
||||
HostWebUiTab,
|
||||
HostProxmoxTab,
|
||||
HostProxmoxStatsTab,
|
||||
HostFilesTab,
|
||||
} from "./HostEditorFeatureTabs";
|
||||
import { HostEditorGeneralTab } from "./HostEditorGeneralTab";
|
||||
@@ -2597,7 +2598,10 @@ export function HostEditor({
|
||||
)}
|
||||
|
||||
{activeTab === "proxmox" && (
|
||||
<HostProxmoxTab form={form} setField={setField} />
|
||||
<>
|
||||
<HostProxmoxTab form={form} setField={setField} />
|
||||
<HostProxmoxStatsTab form={form} setField={setField} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "files" && (
|
||||
|
||||
@@ -317,6 +317,26 @@ export function HostProxmoxTab({
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
|
||||
export function HostProxmoxStatsTab({
|
||||
form,
|
||||
setField,
|
||||
}: {
|
||||
form: HostEditorForm;
|
||||
setField: SetHostField;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("hosts.enableProxmoxStats")}
|
||||
icon={<Server className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-0 py-1">
|
||||
<SettingRow
|
||||
label={t("hosts.enableProxmoxStats")}
|
||||
description={t("hosts.enableProxmoxStatsDesc")}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { HostManager } from "@/sidebar/HostManager";
|
||||
import { HostShareModal } from "@/sidebar/HostShareModal";
|
||||
import { HostExportDialog } from "@/sidebar/HostExportDialog";
|
||||
import { CustomizeSidebarPanel } from "@/sidebar/CustomizeSidebarPanel";
|
||||
import { ProxmoxDiscoverDialog } from "@/components/proxmox/ProxmoxDiscoverDialog";
|
||||
import { ProxmoxDiscoverDialog } from "../../../plugins/proxmox/frontend/ProxmoxDiscoverDialog";
|
||||
import { Button } from "@/components/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveProxmoxImportAuth } from "../../../components/proxmox/proxmox-import-auth";
|
||||
import { resolveProxmoxImportAuth } from "../../../../../plugins/proxmox/frontend/proxmox-import-auth";
|
||||
|
||||
describe("resolveProxmoxImportAuth", () => {
|
||||
it("inherits an available credential even with the default 'password' authType", () => {
|
||||
@@ -29,7 +29,8 @@
|
||||
"include": [
|
||||
"plugins/docker/backend/**/*.ts",
|
||||
"plugins/host-metrics/backend/**/*.ts",
|
||||
"plugins/ai/backend/**/*.ts"
|
||||
"plugins/ai/backend/**/*.ts",
|
||||
"plugins/proxmox/backend/**/*.ts"
|
||||
],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user