mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: finish the docker plugin (B15)
This commit is contained in:
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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user