mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
chore: lock the plugin boundary and remove 2.9.0 leftovers
This commit is contained in:
1 parent
6489b62ece
commit
2961297989
399 files changed
+3964
-5627
No files matched your search
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "termix-plugin build",
|
||||
"test": "termix-plugin test",
|
||||
"typecheck": "tsc -b ../../tsconfig.plugins.json ../../tsconfig.plugins.frontend.json",
|
||||
"typecheck": "tsc -b ../../tsconfig.plugins.backend.json ../../tsconfig.plugins.frontend.json",
|
||||
"validate": "termix-plugin validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1004,6 +1004,63 @@ router.post("/discover", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /plugin-api/proxmox/import:
|
||||
* post:
|
||||
* summary: Import discovered Proxmox guests as hosts
|
||||
* description: >
|
||||
* Creates one host per entry, owned by the caller, and stores each
|
||||
* entry's proxmox host settings (the guest's source) on the new host.
|
||||
* tags:
|
||||
* - Proxmox
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - hosts
|
||||
* properties:
|
||||
* hosts:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* responses:
|
||||
* 200:
|
||||
* description: How many hosts were created and which failed.
|
||||
* 400:
|
||||
* description: No hosts given.
|
||||
*/
|
||||
router.post("/import", async (req, res) => {
|
||||
const ctx = pluginCtx();
|
||||
const { hosts } = req.body as { hosts?: unknown };
|
||||
if (!Array.isArray(hosts) || hosts.length === 0) {
|
||||
return res.status(400).json({ error: "No hosts to import" });
|
||||
}
|
||||
const result = { success: 0, failed: 0, errors: [] as string[] };
|
||||
for (const entry of hosts as Array<Record<string, unknown>>) {
|
||||
const { pluginSettings, ...input } = entry;
|
||||
try {
|
||||
const created = await ctx.hosts.create(
|
||||
input as unknown as PluginHostCreateInput,
|
||||
);
|
||||
const own = (pluginSettings as Record<string, unknown> | undefined)?.[
|
||||
ctx.pluginId
|
||||
] as Record<string, unknown> | undefined;
|
||||
for (const [key, value] of Object.entries(own ?? {})) {
|
||||
await ctx.settings.setHost(Number(created.id), key, value);
|
||||
}
|
||||
result.success++;
|
||||
} catch (err: unknown) {
|
||||
result.failed++;
|
||||
result.errors.push(`${String(input.name)}: ${getErrorMessage(err)}`);
|
||||
}
|
||||
}
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
/** 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
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSshAuthTypes, useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Server } from "lucide-react";
|
||||
import { Input } from "@/components/input";
|
||||
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
|
||||
import { getCredentials } from "@/main-axios";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import {
|
||||
FakeSwitch,
|
||||
Input,
|
||||
SectionCard,
|
||||
Select2,
|
||||
SettingRow,
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { listCredentials } from "@termix/plugin-sdk/frontend";
|
||||
import type { ProxmoxHostConfig } from "./types";
|
||||
|
||||
interface HostProxmoxForm {
|
||||
@@ -28,19 +32,16 @@ export function HostProxmoxTab({
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getCredentials()
|
||||
.then((res: unknown) => {
|
||||
const raw =
|
||||
(res as { credentials?: unknown })?.credentials ?? res ?? [];
|
||||
const list = (Array.isArray(raw) ? raw : []).map(
|
||||
(c: Record<string, unknown>) => ({
|
||||
id: c.id as number,
|
||||
name: c.name as string,
|
||||
username: (c.username as string | null) ?? null,
|
||||
}),
|
||||
);
|
||||
setCredentials(list);
|
||||
})
|
||||
listCredentials()
|
||||
.then((list) =>
|
||||
setCredentials(
|
||||
list.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
username: c.username ?? null,
|
||||
})),
|
||||
),
|
||||
)
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import { useRef, useState } from "react";
|
||||
import type { HostData } from "@/types/index";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Server, RefreshCw, CheckSquare, Square, Download } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/button";
|
||||
import { Button } from "@termix/plugin-sdk/ui";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/dialog";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import {
|
||||
discoverProxmoxGuestsStream,
|
||||
bulkImportSSHHosts,
|
||||
getSSHHosts,
|
||||
} from "@/main-axios";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
import type { ProxmoxGuest } from "@/types/proxmox";
|
||||
} from "@termix/plugin-sdk/ui";
|
||||
import { Select2 } from "@termix/plugin-sdk/ui";
|
||||
import { discoverProxmoxGuestsStream, importProxmoxHosts } from "./proxmox-api";
|
||||
import { listHosts, type PluginHostRecord } from "@termix/plugin-sdk/frontend";
|
||||
import type { ProxmoxGuest } from "./types";
|
||||
import { resolveProxmoxImportAuth } from "./proxmox-import-auth";
|
||||
|
||||
interface ProxmoxDiscoverDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
hosts: SSHHostWithStatus[];
|
||||
onHostsChanged: (hosts: SSHHostWithStatus[]) => void;
|
||||
hosts: PluginHostRecord[];
|
||||
onHostsChanged: (hosts: PluginHostRecord[]) => void;
|
||||
/** Pre-select a specific host and skip the host picker */
|
||||
preselectedHostId?: number;
|
||||
/** Credential to use for imported hosts */
|
||||
@@ -69,7 +64,11 @@ export function ProxmoxDiscoverDialog({
|
||||
// When opened from the dropdown (no preselectedHostId), only show Proxmox-enabled hosts
|
||||
const sshHosts = hosts.filter(
|
||||
(h) =>
|
||||
!("isFolder" in h) && h.pluginSettings?.proxmox?.enableProxmox === true,
|
||||
!("isFolder" in h) &&
|
||||
(
|
||||
h.pluginSettings as
|
||||
Record<string, Record<string, unknown> | undefined> | undefined
|
||||
)?.proxmox?.enableProxmox === true,
|
||||
);
|
||||
|
||||
// The Proxmox host the discovery runs against — imported guests are grouped
|
||||
@@ -189,11 +188,11 @@ export function ProxmoxDiscoverDialog({
|
||||
}));
|
||||
|
||||
const result = toImport.length
|
||||
? await bulkImportSSHHosts(toImport as unknown as HostData[], false)
|
||||
: { success: 0, updated: 0, skipped: 0, failed: 0 };
|
||||
? await importProxmoxHosts(toImport)
|
||||
: { success: 0, failed: 0, errors: [] };
|
||||
|
||||
if (toImport.length) {
|
||||
const updated = await getSSHHosts();
|
||||
const updated = await listHosts();
|
||||
onHostsChanged(updated);
|
||||
window.dispatchEvent(new CustomEvent("termix:hosts-changed"));
|
||||
}
|
||||
@@ -202,9 +201,6 @@ export function ProxmoxDiscoverDialog({
|
||||
result.success
|
||||
? t("hosts.proxmoxResultImported", { count: result.success })
|
||||
: null,
|
||||
result.updated
|
||||
? t("hosts.proxmoxResultUpdated", { count: result.updated })
|
||||
: null,
|
||||
result.failed
|
||||
? t("hosts.proxmoxResultFailed", { count: result.failed })
|
||||
: null,
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
type TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import { ComponentSlot, DropdownMenuItem } from "@termix/plugin-sdk/ui";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
import type { PluginHostRecord } from "@termix/plugin-sdk/frontend";
|
||||
import { setProxmoxApp } from "./proxmox-api";
|
||||
import { ProxmoxDiscoverDialog } from "./ProxmoxDiscoverDialog";
|
||||
import { HostProxmoxTab } from "./HostProxmoxTab";
|
||||
import { ProxmoxStatsTab } from "./stats/ProxmoxStatsTab";
|
||||
import ProxmoxStatsApp from "./stats/ProxmoxStatsApp";
|
||||
import { HostProxmoxStatsTab } from "./stats/HostProxmoxStatsTab";
|
||||
import type { SSHHost } from "@/types";
|
||||
|
||||
interface DiscoverRequest {
|
||||
hostId?: number;
|
||||
@@ -79,7 +79,7 @@ function ProxmoxHostSection({ form, updateForm }: HostEditorSectionProps) {
|
||||
function ProxmoxStatsTabView({ sshHost, label, isVisible }: TabProps) {
|
||||
return (
|
||||
<ProxmoxStatsTab
|
||||
hostConfig={sshHost as unknown as SSHHost}
|
||||
hostConfig={sshHost as never}
|
||||
title={label}
|
||||
isVisible={isVisible}
|
||||
isTopbarOpen={false}
|
||||
@@ -93,6 +93,8 @@ function ProxmoxStatsStandalone({ hostId }: StandaloneViewProps) {
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
setProxmoxApp(app);
|
||||
app.onDispose(() => setProxmoxApp(null));
|
||||
// The dialog lives in the hosts panel; the menu item and host action open
|
||||
// it through this, created per activation.
|
||||
const openers = new Set<Opener>();
|
||||
@@ -104,8 +106,8 @@ export function activate(app: TermixApp): void {
|
||||
hosts,
|
||||
onHostsChanged,
|
||||
}: {
|
||||
hosts: SSHHostWithStatus[];
|
||||
onHostsChanged: (hosts: SSHHostWithStatus[]) => void;
|
||||
hosts: PluginHostRecord[];
|
||||
onHostsChanged: (hosts: PluginHostRecord[]) => void;
|
||||
}) {
|
||||
const [request, setRequest] = useState<DiscoverRequest | null>(null);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { TermixApp } from "@termix/plugin-sdk/frontend";
|
||||
import type { ProxmoxDiscoverResult } from "./types";
|
||||
|
||||
let current: TermixApp | null = null;
|
||||
|
||||
/** Set in activate, cleared on deactivate. */
|
||||
export function setProxmoxApp(app: TermixApp | null): void {
|
||||
current = app;
|
||||
}
|
||||
|
||||
function app(): TermixApp {
|
||||
if (!current) throw new Error("The Proxmox plugin is not active");
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a guest discovery over the server-sent event stream and reports
|
||||
* progress. Returns a function that stops listening.
|
||||
*/
|
||||
export function discoverProxmoxGuestsStream(
|
||||
hostId: number,
|
||||
handlers: {
|
||||
onProgress?: (done: number, total: number) => void;
|
||||
onResult: (result: ProxmoxDiscoverResult) => void;
|
||||
onError: (message: string) => void;
|
||||
},
|
||||
): () => void {
|
||||
const controller = new AbortController();
|
||||
let settled = false;
|
||||
const finish = () => {
|
||||
settled = true;
|
||||
controller.abort();
|
||||
};
|
||||
|
||||
const handleEvent = (event: string, data: string) => {
|
||||
if (event === "progress") {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
handlers.onProgress?.(parsed.done, parsed.total);
|
||||
} catch {
|
||||
// ignore malformed progress frames
|
||||
}
|
||||
} else if (event === "result") {
|
||||
finish();
|
||||
try {
|
||||
handlers.onResult(JSON.parse(data));
|
||||
} catch {
|
||||
handlers.onError("Failed to parse discovery result");
|
||||
}
|
||||
} else if (event === "fail") {
|
||||
finish();
|
||||
let message = "Discovery failed";
|
||||
try {
|
||||
message = JSON.parse(data).message || message;
|
||||
} catch {
|
||||
// keep default message
|
||||
}
|
||||
handlers.onError(message);
|
||||
}
|
||||
};
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await app().fetch(
|
||||
`/discover/stream?hostId=${encodeURIComponent(String(hostId))}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (!response.ok || !response.body) throw new Error("stream failed");
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
let split = buffer.indexOf("\n\n");
|
||||
while (split !== -1) {
|
||||
const frame = buffer.slice(0, split);
|
||||
buffer = buffer.slice(split + 2);
|
||||
let event = "message";
|
||||
const data: string[] = [];
|
||||
for (const line of frame.split("\n")) {
|
||||
if (line.startsWith("event:")) event = line.slice(6).trim();
|
||||
else if (line.startsWith("data:")) data.push(line.slice(5).trim());
|
||||
}
|
||||
if (data.length) handleEvent(event, data.join("\n"));
|
||||
split = buffer.indexOf("\n\n");
|
||||
}
|
||||
}
|
||||
if (!settled) {
|
||||
settled = true;
|
||||
handlers.onError("Discovery connection lost");
|
||||
}
|
||||
} catch {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
handlers.onError("Discovery connection lost");
|
||||
}
|
||||
})();
|
||||
|
||||
return finish;
|
||||
}
|
||||
|
||||
export interface ProxmoxImportResult {
|
||||
success: number;
|
||||
failed: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export async function importProxmoxHosts(
|
||||
hosts: Record<string, unknown>[],
|
||||
): Promise<ProxmoxImportResult> {
|
||||
const response = await app().api.post<ProxmoxImportResult>("/import", {
|
||||
hosts,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
@@ -133,3 +133,21 @@ export interface ProxmoxHostConfig {
|
||||
errors: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProxmoxGuest {
|
||||
name: string;
|
||||
vmid: number;
|
||||
type: "qemu" | "lxc";
|
||||
node: string;
|
||||
status: string;
|
||||
ip: string | null;
|
||||
connectionType: "ssh" | "rdp";
|
||||
enableDocker: boolean;
|
||||
}
|
||||
|
||||
export interface ProxmoxDiscoverResult {
|
||||
guests: ProxmoxGuest[];
|
||||
credentialId: number | null;
|
||||
defaultCredentialId: number | null;
|
||||
jumpHosts?: unknown[] | null;
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/types/*": ["../../src/types/*"],
|
||||
"@/*": ["../../src/ui/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
|
||||
}
|
||||
Reference in new issue
Block a user