mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 14:15:24 +00:00
feat: load plugin frontends at runtime and remove plugin ids from the shell
This commit is contained in:
1 parent
49f0286a14
commit
e88d5a52e3
721 files changed
+54265
-44416
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { DockerManager } from "./DockerManager.tsx";
|
||||
import { FullScreenAppWrapper } from "@/features/FullScreenAppWrapper.tsx";
|
||||
import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx";
|
||||
|
||||
@@ -15,8 +15,9 @@ import {
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { SSHHost, DockerContainer, DockerValidation } from "@/types";
|
||||
import { logActivity, getSSHHosts } from "@/main-axios";
|
||||
import {
|
||||
connectDockerSession,
|
||||
disconnectDockerSession,
|
||||
@@ -25,9 +26,7 @@ import {
|
||||
keepaliveDockerSession,
|
||||
verifyDockerTOTP,
|
||||
verifyDockerWarpgate,
|
||||
logActivity,
|
||||
getSSHHosts,
|
||||
} from "@/main-axios.ts";
|
||||
} from "./docker-api";
|
||||
import { ContainerList } from "./components/ContainerList.tsx";
|
||||
import { ContainerTable } from "./components/ContainerTable.tsx";
|
||||
import { ContainerDetail } from "./components/ContainerDetail.tsx";
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box } from "lucide-react";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { DockerWidgetEditForm } from "./DockerWidgetEditForm";
|
||||
import type {
|
||||
DockerWidgetConfig,
|
||||
WidgetComponentProps,
|
||||
WidgetTypeDefinition,
|
||||
} from "@/types/homepage-types";
|
||||
import { GRID_SIZE } from "@/types/homepage-types";
|
||||
import { getSSHHosts } from "@/api/ssh-host-management-api";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
import { DockerManager } from "./DockerManager";
|
||||
import type { SSHHost } from "@/types/index";
|
||||
import { WidgetTitle } from "@/features/homepage/widgets/WidgetTitle";
|
||||
|
||||
function DockerWidget({
|
||||
widget,
|
||||
config,
|
||||
}: WidgetComponentProps<DockerWidgetConfig>) {
|
||||
const { t } = useTranslation();
|
||||
const [host, setHost] = useState<SSHHostWithStatus | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.hostId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
getSSHHosts()
|
||||
.then((hosts) => {
|
||||
const found = hosts.find(
|
||||
(h) => h.id === config.hostId && h.enableDocker,
|
||||
);
|
||||
setHost(found ?? null);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [config.hostId]);
|
||||
|
||||
if (!config.hostId) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full gap-2 text-muted-foreground/60">
|
||||
<Box size={20} />
|
||||
<span className="text-xs">{t("homepage.widgetNoHostSelected")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full text-xs text-muted-foreground/60">
|
||||
{t("homepage.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!host) {
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-full text-xs text-muted-foreground/60">
|
||||
{t("homepage.widgetNoHostSelected")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full h-full overflow-hidden">
|
||||
<WidgetTitle title={widget.title} icon={<Box size={11} />} />
|
||||
<div
|
||||
className="flex-1 overflow-hidden"
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DockerManager
|
||||
hostConfig={host as unknown as SSHHost}
|
||||
title={host.name || host.ip}
|
||||
isVisible={true}
|
||||
isTopbarOpen={false}
|
||||
embedded={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Registered by the plugin while it runs. */
|
||||
export const dockerWidget: WidgetTypeDefinition<DockerWidgetConfig> = {
|
||||
id: "docker_widget",
|
||||
name: "Docker Manager",
|
||||
description: "Embedded Docker container manager for a configured host",
|
||||
category: "system",
|
||||
icon: <Box size={14} />,
|
||||
defaultConfig: { hostId: 0 },
|
||||
defaultSize: { w: GRID_SIZE * 20, h: GRID_SIZE * 14 },
|
||||
minSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 8 },
|
||||
component: DockerWidget,
|
||||
editFormComponent: DockerWidgetEditForm,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type {
|
||||
DockerWidgetConfig,
|
||||
WidgetEditFormProps,
|
||||
} from "@/types/homepage-types";
|
||||
import { SingleHostEditForm } from "@/features/homepage/dialogs/SingleHostEditForm";
|
||||
|
||||
export function DockerWidgetEditForm({
|
||||
config,
|
||||
onChange,
|
||||
}: WidgetEditFormProps<DockerWidgetConfig>) {
|
||||
return (
|
||||
<SingleHostEditForm
|
||||
hostId={config.hostId}
|
||||
onChange={(hostId) => onChange({ ...config, hostId })}
|
||||
filter={(h) => !!(h.enableSsh && h.enableDocker)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Box } from "lucide-react";
|
||||
import { Select2 } from "@/components/select2";
|
||||
import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card";
|
||||
import type { HostEditorForm } from "@/sidebar/HostEditorData";
|
||||
|
||||
type SetHostField = <K extends keyof HostEditorForm>(
|
||||
key: K,
|
||||
value: HostEditorForm[K],
|
||||
) => void;
|
||||
|
||||
export function HostDockerTab({
|
||||
form,
|
||||
setField,
|
||||
}: {
|
||||
form: HostEditorForm;
|
||||
setField: SetHostField;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const dockerConfig = form.dockerConfig ?? { runtime: "docker" as const };
|
||||
const runtime =
|
||||
dockerConfig.runtime === "podman"
|
||||
? ("podman" as const)
|
||||
: ("docker" as const);
|
||||
|
||||
return (
|
||||
<SectionCard
|
||||
title={t("hosts.dockerIntegration")}
|
||||
icon={<Box className="size-3.5" />}
|
||||
>
|
||||
<div className="flex flex-col gap-4 py-3">
|
||||
<SettingRow
|
||||
label={t("hosts.enableDockerMonitor")}
|
||||
description={t("hosts.enableDockerMonitorDesc")}
|
||||
>
|
||||
<FakeSwitch
|
||||
checked={form.enableDocker}
|
||||
onChange={(v) => setField("enableDocker", v)}
|
||||
/>
|
||||
</SettingRow>
|
||||
{form.enableDocker && (
|
||||
<SettingRow
|
||||
label={t("hosts.containerRuntime")}
|
||||
description={t("hosts.containerRuntimeDesc")}
|
||||
>
|
||||
<Select2
|
||||
value={runtime}
|
||||
onChange={(e) =>
|
||||
setField("dockerConfig", {
|
||||
...dockerConfig,
|
||||
runtime: e.target.value as "docker" | "podman",
|
||||
})
|
||||
}
|
||||
className="h-7 w-44 text-xs border border-border bg-background px-2 outline-none focus:ring-1 focus:ring-ring"
|
||||
>
|
||||
<option value="docker">
|
||||
{t("hosts.containerRuntimeDocker")}
|
||||
</option>
|
||||
<option value="podman">
|
||||
{t("hosts.containerRuntimePodman")}
|
||||
</option>
|
||||
</Select2>
|
||||
</SettingRow>
|
||||
)}
|
||||
</div>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { Terminal as TerminalIcon, Power, PowerOff } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { SSHHost } from "@/types";
|
||||
import { isElectron } from "@/main-axios.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { resolveTermixThemeColors } from "@/features/terminal/terminal-theme";
|
||||
import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes";
|
||||
import { ensureTerminalFontsLoaded } from "@/features/terminal/terminal-global-styles";
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { DockerContainer } from "@/types";
|
||||
import {
|
||||
startDockerContainer,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
pauseDockerContainer,
|
||||
unpauseDockerContainer,
|
||||
removeDockerContainer,
|
||||
} from "@/main-axios.ts";
|
||||
} from "../docker-api";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
|
||||
interface ContainerCardProps {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Settings,
|
||||
Terminal,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { DockerContainer, SSHHost } from "@/types";
|
||||
import { LogViewer } from "./LogViewer.tsx";
|
||||
import { ContainerStats } from "./ContainerStats.tsx";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Box } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { DockerContainer } from "@/types";
|
||||
import { ContainerCard } from "./ContainerCard.tsx";
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
RefreshCw,
|
||||
} from "lucide-react";
|
||||
import type { DockerStats } from "@/types";
|
||||
import { getContainerStats } from "@/main-axios.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getContainerStats } from "../docker-api";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { SectionCard } from "@/components/section-card";
|
||||
import { DockerBadge } from "./ContainerCard.tsx";
|
||||
import { useAdaptivePolling } from "@/hooks/use-adaptive-polling.ts";
|
||||
|
||||
@@ -2,10 +2,10 @@ import { getErrorMessage } from "@/lib/error-message.js";
|
||||
import React from "react";
|
||||
import { Box, List, Play, RefreshCw, Square, Terminal } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import type { DockerContainer } from "@/types";
|
||||
import { startDockerContainer, stopDockerContainer } from "@/main-axios.ts";
|
||||
import { startDockerContainer, stopDockerContainer } from "../docker-api";
|
||||
import { DockerBadge } from "./ContainerCard.tsx";
|
||||
|
||||
type DetailTab = "logs" | "stats" | "console";
|
||||
|
||||
@@ -5,9 +5,9 @@ import { Input } from "@/components/input.tsx";
|
||||
import { Separator } from "@/components/separator.tsx";
|
||||
import { Download, RefreshCw, Search, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||
import type { DockerLogOptions } from "@/types";
|
||||
import { getContainerLogs, downloadContainerLogs } from "@/main-axios.ts";
|
||||
import { getContainerLogs, downloadContainerLogs } from "../docker-api";
|
||||
import { useAdaptivePolling } from "@/hooks/use-adaptive-polling.ts";
|
||||
import { Select2 } from "@/components/select2";
|
||||
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
import axios from "axios";
|
||||
import { dockerApi, handleApiError } from "@/main-axios";
|
||||
import type {
|
||||
DockerContainer,
|
||||
DockerLogOptions,
|
||||
DockerStats,
|
||||
DockerValidation,
|
||||
} from "@/types/index";
|
||||
|
||||
type ApiConnectionLog = {
|
||||
type: "info" | "success" | "warning" | "error";
|
||||
stage: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type ConnectErrorResponse = {
|
||||
error?: string;
|
||||
message?: string;
|
||||
connectionLogs?: ApiConnectionLog[];
|
||||
requires_totp?: boolean;
|
||||
requires_warpgate?: boolean;
|
||||
sessionId?: string;
|
||||
prompt?: string;
|
||||
url?: string;
|
||||
securityKey?: string;
|
||||
status?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export async function connectDockerSession(
|
||||
sessionId: string,
|
||||
hostId: number,
|
||||
config?: {
|
||||
userProvidedPassword?: string;
|
||||
userProvidedSshKey?: string;
|
||||
userProvidedKeyPassword?: string;
|
||||
forceKeyboardInteractive?: boolean;
|
||||
useSocks5?: boolean;
|
||||
socks5Host?: string;
|
||||
socks5Port?: number;
|
||||
socks5Username?: string;
|
||||
socks5Password?: string;
|
||||
socks5ProxyChain?: unknown;
|
||||
},
|
||||
): Promise<{
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
requires_totp?: boolean;
|
||||
prompt?: string;
|
||||
isPassword?: boolean;
|
||||
status?: string;
|
||||
reason?: string;
|
||||
connectionLogs?: ApiConnectionLog[];
|
||||
requires_warpgate?: boolean;
|
||||
url?: string;
|
||||
securityKey?: string;
|
||||
}> {
|
||||
try {
|
||||
const response = await dockerApi.post("/ssh/connect", {
|
||||
sessionId,
|
||||
hostId,
|
||||
...config,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
axios.isAxiosError<ConnectErrorResponse>(error) &&
|
||||
error.response?.data?.status === "auth_required"
|
||||
) {
|
||||
return error.response.data;
|
||||
}
|
||||
if (
|
||||
axios.isAxiosError<ConnectErrorResponse>(error) &&
|
||||
error.response?.data?.requires_totp
|
||||
) {
|
||||
return error.response.data;
|
||||
}
|
||||
if (
|
||||
axios.isAxiosError<ConnectErrorResponse>(error) &&
|
||||
error.response?.data?.requires_warpgate
|
||||
) {
|
||||
return error.response.data;
|
||||
}
|
||||
if (
|
||||
axios.isAxiosError<ConnectErrorResponse>(error) &&
|
||||
error.response?.data?.connectionLogs
|
||||
) {
|
||||
const data = error.response.data;
|
||||
const errorWithLogs = new Error(
|
||||
data.error || data.message || error.message,
|
||||
);
|
||||
Object.assign(errorWithLogs, {
|
||||
connectionLogs: data.connectionLogs,
|
||||
});
|
||||
throw errorWithLogs;
|
||||
}
|
||||
throw handleApiError(error, "connect to Docker SSH session");
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyDockerTOTP(
|
||||
sessionId: string,
|
||||
totpCode: string,
|
||||
): Promise<{ status: string; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post("/ssh/connect-totp", {
|
||||
sessionId,
|
||||
totpCode,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "verify Docker TOTP");
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyDockerWarpgate(
|
||||
sessionId: string,
|
||||
): Promise<{ status: string; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post("/ssh/connect-warpgate", {
|
||||
sessionId,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "verify Docker Warpgate");
|
||||
}
|
||||
}
|
||||
|
||||
export async function disconnectDockerSession(
|
||||
sessionId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post("/ssh/disconnect", {
|
||||
sessionId,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "disconnect from Docker SSH session");
|
||||
}
|
||||
}
|
||||
|
||||
export async function keepaliveDockerSession(
|
||||
sessionId: string,
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
const response = await dockerApi.post("/ssh/keepalive", {
|
||||
sessionId,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "keepalive Docker SSH session");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDockerSessionStatus(
|
||||
sessionId: string,
|
||||
): Promise<{ success: boolean; connected: boolean }> {
|
||||
try {
|
||||
const response = await dockerApi.get("/ssh/status", {
|
||||
params: { sessionId },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get Docker session status");
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateDockerAvailability(
|
||||
sessionId: string,
|
||||
): Promise<DockerValidation> {
|
||||
try {
|
||||
const response = await dockerApi.get(`/validate/${sessionId}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "validate Docker availability");
|
||||
}
|
||||
}
|
||||
|
||||
export async function listDockerContainers(
|
||||
sessionId: string,
|
||||
all: boolean = true,
|
||||
): Promise<DockerContainer[]> {
|
||||
try {
|
||||
const response = await dockerApi.get(`/containers/${sessionId}`, {
|
||||
params: { all },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "list Docker containers");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDockerContainerDetails(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
): Promise<DockerContainer> {
|
||||
try {
|
||||
const response = await dockerApi.get(
|
||||
`/containers/${sessionId}/${containerId}`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get Docker container details");
|
||||
}
|
||||
}
|
||||
|
||||
export async function startDockerContainer(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post(
|
||||
`/containers/${sessionId}/${containerId}/start`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "start Docker container");
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopDockerContainer(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post(
|
||||
`/containers/${sessionId}/${containerId}/stop`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "stop Docker container");
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartDockerContainer(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post(
|
||||
`/containers/${sessionId}/${containerId}/restart`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "restart Docker container");
|
||||
}
|
||||
}
|
||||
|
||||
export async function pauseDockerContainer(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post(
|
||||
`/containers/${sessionId}/${containerId}/pause`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "pause Docker container");
|
||||
}
|
||||
}
|
||||
|
||||
export async function unpauseDockerContainer(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.post(
|
||||
`/containers/${sessionId}/${containerId}/unpause`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "unpause Docker container");
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeDockerContainer(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
force: boolean = false,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const response = await dockerApi.delete(
|
||||
`/containers/${sessionId}/${containerId}/remove`,
|
||||
{
|
||||
params: { force },
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "remove Docker container");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContainerLogs(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
options?: DockerLogOptions,
|
||||
): Promise<{ logs: string }> {
|
||||
try {
|
||||
const response = await dockerApi.get(
|
||||
`/containers/${sessionId}/${containerId}/logs`,
|
||||
{
|
||||
params: options,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get container logs");
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadContainerLogs(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
options?: DockerLogOptions,
|
||||
): Promise<Blob> {
|
||||
try {
|
||||
const response = await dockerApi.get(
|
||||
`/containers/${sessionId}/${containerId}/logs`,
|
||||
{
|
||||
params: { ...options, download: true },
|
||||
responseType: "blob",
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "download container logs");
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContainerStats(
|
||||
sessionId: string,
|
||||
containerId: string,
|
||||
): Promise<DockerStats> {
|
||||
try {
|
||||
const response = await dockerApi.get(
|
||||
`/containers/${sessionId}/${containerId}/stats`,
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "get container stats");
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Frontend half of the docker plugin.
|
||||
*
|
||||
* Registers the container-management tab through the existing extension
|
||||
* seams (registerRailItem / registerTabComponent) the same way
|
||||
* ssh-terminal's frontend does, and the host-editor Docker tab through the
|
||||
* new registerHostEditorTab seam built for this plugin.
|
||||
*
|
||||
* HostDockerTab 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.
|
||||
*/
|
||||
|
||||
export const id = "docker";
|
||||
export const tabId = "docker";
|
||||
export const hostEditorTabId = "docker";
|
||||
|
||||
export async function register({
|
||||
registerRailItem,
|
||||
registerTabComponent,
|
||||
registerHostEditorTab,
|
||||
icons,
|
||||
}) {
|
||||
registerTabComponent(tabId, () =>
|
||||
import("./DockerManager.tsx").then((m) => ({
|
||||
default: m.DockerManager,
|
||||
})),
|
||||
);
|
||||
|
||||
registerRailItem({
|
||||
id: tabId,
|
||||
icon: icons.Box,
|
||||
labelKey: "nav.docker",
|
||||
kind: "tab",
|
||||
});
|
||||
|
||||
if (registerHostEditorTab) {
|
||||
const { HostDockerTab } =
|
||||
await import("../../../../src/ui/sidebar/HostEditorFeatureTabs.tsx");
|
||||
registerHostEditorTab({
|
||||
id: hostEditorTabId,
|
||||
labelKey: "hosts.tabDocker",
|
||||
icon: icons.Box,
|
||||
component: HostDockerTab,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregister({
|
||||
unregisterRailItem,
|
||||
unregisterTabComponent,
|
||||
unregisterHostEditorTab,
|
||||
}) {
|
||||
unregisterRailItem(tabId);
|
||||
unregisterTabComponent(tabId);
|
||||
unregisterHostEditorTab?.(hostEditorTabId);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { ComponentType } from "react";
|
||||
import { Box } from "lucide-react";
|
||||
import type {
|
||||
HomepageWidgetContribution,
|
||||
HostEditorSectionProps,
|
||||
StandaloneViewProps,
|
||||
TabProps,
|
||||
TermixApp,
|
||||
} from "@termix/plugin-sdk/frontend";
|
||||
import type { SSHHost } from "@/types";
|
||||
import { DockerManager } from "./DockerManager";
|
||||
import DockerApp from "./DockerApp";
|
||||
import { HostDockerTab } from "./HostDockerTab";
|
||||
import { dockerWidget } from "./DockerWidget";
|
||||
|
||||
function DockerTab({ sshHost, label, isVisible }: TabProps) {
|
||||
return (
|
||||
<DockerManager
|
||||
hostConfig={sshHost as unknown as SSHHost}
|
||||
title={label}
|
||||
isVisible={isVisible}
|
||||
isTopbarOpen={false}
|
||||
embedded={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DockerStandalone({ hostId }: StandaloneViewProps) {
|
||||
return <DockerApp hostId={hostId} />;
|
||||
}
|
||||
|
||||
function DockerHostSection({ form, setField }: HostEditorSectionProps) {
|
||||
return (
|
||||
<HostDockerTab
|
||||
form={form}
|
||||
setField={setField as Parameters<typeof HostDockerTab>[0]["setField"]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function activate(app: TermixApp): void {
|
||||
app.registerTab("docker", DockerTab, {
|
||||
icon: Box,
|
||||
titleKey: "nav.docker",
|
||||
requiresHost: true,
|
||||
noHostMessageKey: "docker.noHostSelected",
|
||||
persistent: true,
|
||||
activityTypes: ["docker"],
|
||||
standalone: DockerStandalone,
|
||||
preload: () => import("./DockerManager"),
|
||||
});
|
||||
|
||||
app.registerHostAction({
|
||||
id: "docker",
|
||||
titleKey: "nav.docker",
|
||||
icon: Box,
|
||||
kind: "open",
|
||||
order: 30,
|
||||
tabType: "docker",
|
||||
copyUrlView: "docker",
|
||||
when: (host) => !!host.enableSsh && !!host.enableDocker,
|
||||
});
|
||||
|
||||
app.registerHostEditorSection({
|
||||
id: "docker",
|
||||
group: "ssh",
|
||||
titleKey: "hosts.tabDocker",
|
||||
icon: Box,
|
||||
order: 30,
|
||||
component: DockerHostSection,
|
||||
});
|
||||
|
||||
app.registerHomepageWidget(
|
||||
dockerWidget as unknown as HomepageWidgetContribution,
|
||||
);
|
||||
|
||||
app.registerSlotContribution("onboarding.features", {
|
||||
actionId: "docker.feature",
|
||||
titleKey: "onboarding.feature_docker",
|
||||
descriptionKey: "onboarding.feature_docker_desc",
|
||||
icon: Box as ComponentType<{ className?: string }>,
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user