feat: finish the docker plugin (B15)

This commit is contained in:
LukeGus committed 2026-09-24 11:58:20 -05:00
1 parent 0a3f05d99d
commit 993a91c899
167 files changed
+26299 -5136

No files matched your search

@@ -1,39 +1,42 @@
import { getErrorMessage } from "@/lib/error-message.js";
import { getErrorMessage } from "../error-message";
import {
Button,
Card,
CardContent,
ConnectionLogProvider,
ConnectionScreen,
DEFAULT_TERMINAL_CONFIG,
RobustClipboardProvider,
Select2,
TERMINAL_FONTS,
copyToClipboard,
ensureTerminalFontsLoaded,
isElectron,
pluginWsUrl,
readFromClipboard,
resolveConnectionOrigin,
resolveTermixThemeColors,
useAppTheme as useTheme,
useConnectionLog,
} from "@termix/plugin-sdk/ui";
import type { DockerHost } from "../types";
import React from "react";
import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit";
import { ClipboardAddon } from "@xterm/addon-clipboard";
import { RobustClipboardProvider } from "@/lib/clipboard-provider";
import { copyToClipboard, readFromClipboard } from "@/lib/clipboard";
import { WebLinksAddon } from "@xterm/addon-web-links";
import { Button } from "@/components/button.tsx";
import { Select2 } from "@/components/select2";
import { Card, CardContent } from "@/components/card.tsx";
import { resolveConnectionOrigin } from "@/lib/connection-origin.ts";
import { pluginWsUrl } from "@/lib/plugin-transport";
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,
useConnectionRetry,
} from "@termix/plugin-sdk/frontend";
import { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme";
import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes";
import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles";
import { useTheme } from "@/components/theme-provider";
import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx";
import {
ConnectionLogProvider,
useConnectionLog,
} from "@/ssh/connection-log/ConnectionLogContext.tsx";
interface ConsoleTerminalProps {
containerId: string;
containerName: string;
containerState: string;
hostConfig: SSHHost;
hostConfig: DockerHost;
}
export function ConsoleTerminal(
@@ -58,7 +61,12 @@ function ConsoleTerminalInner({
const { addLog, clearLogs } = useConnectionLog();
const terminalConfig = React.useMemo(
() => ({ ...DEFAULT_TERMINAL_CONFIG, ...hostConfig.terminalConfig }),
() => ({
...DEFAULT_TERMINAL_CONFIG,
...(hostConfig.terminalConfig as Partial<
typeof DEFAULT_TERMINAL_CONFIG
> | null),
}),
[hostConfig.terminalConfig],
);
@@ -321,7 +329,11 @@ function ConsoleTerminalInner({
JSON.stringify({
type: "connect",
data: {
hostConfig,
hostConfig: {
id: hostConfig.id,
syncId: hostConfig.syncId ?? null,
ip: hostConfig.ip,
},
containerId,
shell: selectedShell,
cols,
@@ -1,7 +1,7 @@
import { getErrorMessage } from "@/lib/error-message.js";
import { getErrorMessage } from "../error-message";
import { Button, Card, useConfirmation } from "@termix/plugin-sdk/ui";
import type { DockerContainer } from "../types";
import React from "react";
import { Card } from "@/components/card.tsx";
import { Button } from "@/components/button.tsx";
import {
Box,
Play,
@@ -14,16 +14,7 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import type { DockerContainer } from "@/types";
import {
startDockerContainer,
stopDockerContainer,
restartDockerContainer,
pauseDockerContainer,
unpauseDockerContainer,
removeDockerContainer,
} from "../docker-api";
import { useConfirmation } from "@/hooks/use-confirmation.ts";
import { useDockerApi } from "../docker-api";
interface ContainerCardProps {
container: DockerContainer;
@@ -61,6 +52,7 @@ export function ContainerCard({
}: ContainerCardProps): React.ReactElement {
const { t } = useTranslation();
const { confirmWithToast } = useConfirmation();
const docker = useDockerApi();
const [isStarting, setIsStarting] = React.useState(false);
const [isStopping, setIsStopping] = React.useState(false);
const [isRestarting, setIsRestarting] = React.useState(false);
@@ -82,7 +74,7 @@ export function ContainerCard({
e.stopPropagation();
setIsStarting(true);
try {
await startDockerContainer(sessionId, container.id);
await docker.containerAction(sessionId, container.id, "start");
toast.success(t("docker.containerStarted", { name: containerName }));
onRefresh?.();
} catch (err) {
@@ -100,7 +92,7 @@ export function ContainerCard({
e.stopPropagation();
setIsStopping(true);
try {
await stopDockerContainer(sessionId, container.id);
await docker.containerAction(sessionId, container.id, "stop");
toast.success(t("docker.containerStopped", { name: containerName }));
onRefresh?.();
} catch (err) {
@@ -118,7 +110,7 @@ export function ContainerCard({
e.stopPropagation();
setIsRestarting(true);
try {
await restartDockerContainer(sessionId, container.id);
await docker.containerAction(sessionId, container.id, "restart");
toast.success(t("docker.containerRestarted", { name: containerName }));
onRefresh?.();
} catch (err) {
@@ -137,10 +129,10 @@ export function ContainerCard({
setIsPausing(true);
try {
if (container.state === "paused") {
await unpauseDockerContainer(sessionId, container.id);
await docker.containerAction(sessionId, container.id, "unpause");
toast.success(t("docker.containerUnpaused", { name: containerName }));
} else {
await pauseDockerContainer(sessionId, container.id);
await docker.containerAction(sessionId, container.id, "pause");
toast.success(t("docker.containerPaused", { name: containerName }));
}
onRefresh?.();
@@ -166,7 +158,7 @@ export function ContainerCard({
async () => {
setIsRemoving(true);
try {
await removeDockerContainer(
await docker.removeContainer(
sessionId,
container.id,
container.state === "running",
@@ -1,7 +1,6 @@
import { Button, Card, Separator } from "@termix/plugin-sdk/ui";
import type { DockerContainer, DockerHost } from "../types";
import React from "react";
import { Button } from "@/components/button.tsx";
import { Card } from "@/components/card.tsx";
import { Separator } from "@/components/separator.tsx";
import {
Activity,
ArrowLeft,
@@ -11,7 +10,6 @@ import {
Terminal,
} from "lucide-react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import type { DockerContainer, SSHHost } from "@/types";
import { LogViewer } from "./LogViewer.tsx";
import { ContainerStats } from "./ContainerStats.tsx";
import { ConsoleTerminal } from "./ConsoleTerminal.tsx";
@@ -23,7 +21,7 @@ interface ContainerDetailProps {
sessionId: string;
containerId: string;
containers: DockerContainer[];
hostConfig: SSHHost;
hostConfig: DockerHost;
onBack: () => void;
initialTab?: DetailTab;
}
@@ -1,7 +1,7 @@
import type { DockerContainer } from "../types";
import React from "react";
import { Box } from "lucide-react";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import type { DockerContainer } from "@/types";
import { ContainerCard } from "./ContainerCard.tsx";
interface ContainerListProps {
@@ -1,4 +1,6 @@
import { getErrorMessage } from "@/lib/error-message.js";
import { getErrorMessage } from "../error-message";
import { SectionCard, useAdaptivePolling } from "@termix/plugin-sdk/ui";
import type { DockerStats } from "../types";
import React from "react";
import {
Activity,
@@ -9,12 +11,9 @@ import {
Network,
RefreshCw,
} from "lucide-react";
import type { DockerStats } from "@/types";
import { getContainerStats } from "../docker-api";
import { useDockerApi } 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";
interface ContainerStatsProps {
sessionId: string;
@@ -30,6 +29,7 @@ export function ContainerStats({
containerState,
}: ContainerStatsProps): React.ReactElement {
const { t } = useTranslation();
const docker = useDockerApi();
const [stats, setStats] = React.useState<DockerStats | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
@@ -41,7 +41,7 @@ export function ContainerStats({
setIsLoading(true);
setError(null);
try {
const data = await getContainerStats(sessionId, containerId);
const data = await docker.stats(sessionId, containerId);
const previous = statsRef.current;
const changed =
!previous ||
@@ -58,7 +58,7 @@ export function ContainerStats({
} finally {
setIsLoading(false);
}
}, [sessionId, containerId, containerState, t]);
}, [sessionId, containerId, containerState, t, docker]);
useAdaptivePolling(
fetchStats,
@@ -246,7 +246,7 @@ export function ContainerStats({
</span>
<DockerBadge
state={
containerState as import("@/types").DockerContainer["state"]
containerState as import("../types").DockerContainer["state"]
}
/>
</div>
@@ -1,11 +1,11 @@
import { getErrorMessage } from "@/lib/error-message.js";
import { getErrorMessage } from "../error-message";
import { Button } from "@termix/plugin-sdk/ui";
import type { DockerContainer } from "../types";
import React from "react";
import { Box, List, Play, RefreshCw, Square, Terminal } from "lucide-react";
import { toast } from "sonner";
import { useTranslation } from "@termix/plugin-sdk/frontend";
import { Button } from "@/components/button.tsx";
import type { DockerContainer } from "@/types";
import { startDockerContainer, stopDockerContainer } from "../docker-api";
import { useDockerApi } from "../docker-api";
import { DockerBadge } from "./ContainerCard.tsx";
type DetailTab = "logs" | "stats" | "console";
@@ -30,6 +30,7 @@ export function ContainerTable({
statusFilter = "all",
}: ContainerTableProps): React.ReactElement {
const { t } = useTranslation();
const docker = useDockerApi();
const [pendingId, setPendingId] = React.useState<string | null>(null);
const filtered = React.useMemo(() => {
@@ -56,10 +57,10 @@ export function ContainerTable({
setPendingId(container.id);
try {
if (container.state === "running") {
await stopDockerContainer(sessionId, container.id);
await docker.containerAction(sessionId, container.id, "stop");
toast.success(t("docker.containerStopped", { name: containerName }));
} else {
await startDockerContainer(sessionId, container.id);
await docker.containerAction(sessionId, container.id, "start");
toast.success(t("docker.containerStarted", { name: containerName }));
}
onRefresh?.();
@@ -1,15 +1,17 @@
import { getErrorMessage } from "@/lib/error-message.js";
import { getErrorMessage } from "../error-message";
import {
Button,
Input,
Select2,
Separator,
useAdaptivePolling,
} from "@termix/plugin-sdk/ui";
import type { DockerLogOptions } from "../types";
import React from "react";
import { Button } from "@/components/button.tsx";
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 "@termix/plugin-sdk/frontend";
import type { DockerLogOptions } from "@/types";
import { getContainerLogs, downloadContainerLogs } from "../docker-api";
import { useAdaptivePolling } from "@/hooks/use-adaptive-polling.ts";
import { Select2 } from "@/components/select2";
import { useDockerApi } from "../docker-api";
interface LogViewerProps {
sessionId: string;
@@ -60,6 +62,7 @@ export function LogViewer({
containerName,
}: LogViewerProps): React.ReactElement {
const { t } = useTranslation();
const docker = useDockerApi();
const [rawLogs, setRawLogs] = React.useState<string[]>([]);
const [isLoading, setIsLoading] = React.useState(false);
const [isDownloading, setIsDownloading] = React.useState(false);
@@ -78,7 +81,7 @@ export function LogViewer({
tail: tailLines === "all" ? undefined : parseInt(tailLines, 10),
timestamps: showTimestamps,
};
const data = await getContainerLogs(sessionId, containerId, options);
const data = await docker.logs(sessionId, containerId, options);
const next = data.logs.split("\n").filter(Boolean);
const changed =
next.length !== rawLogsRef.current.length ||
@@ -89,11 +92,13 @@ export function LogViewer({
}
return changed;
} catch (error) {
toast.error(`Failed to fetch logs: ${getErrorMessage(error)}`);
toast.error(
t("docker.failedToFetchLogs", { error: getErrorMessage(error) }),
);
} finally {
setIsLoading(false);
}
}, [sessionId, containerId, tailLines, showTimestamps]);
}, [sessionId, containerId, tailLines, showTimestamps, docker, t]);
React.useEffect(() => {
fetchLogs();
@@ -120,9 +125,10 @@ export function LogViewer({
const handleDownload = async () => {
setIsDownloading(true);
try {
const blob = await downloadContainerLogs(sessionId, containerId, {
const data = await docker.logs(sessionId, containerId, {
timestamps: showTimestamps,
});
const blob = new Blob([data.logs], { type: "text/plain" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
@@ -133,7 +139,9 @@ export function LogViewer({
document.body.removeChild(a);
toast.success(t("docker.logsDownloaded"));
} catch (error) {
toast.error(`Failed to download logs: ${getErrorMessage(error)}`);
toast.error(
t("docker.failedToDownloadLogs", { error: getErrorMessage(error) }),
);
} finally {
setIsDownloading(false);
}