chore: update translations
This commit is contained in:
@@ -311,14 +311,14 @@ export function CredentialEditor({
|
||||
|
||||
const getFriendlyKeyTypeName = (keyType: string): string => {
|
||||
const keyTypeMap: Record<string, string> = {
|
||||
"ssh-rsa": "RSA (SSH)",
|
||||
"ssh-ed25519": "Ed25519 (SSH)",
|
||||
"ecdsa-sha2-nistp256": "ECDSA P-256 (SSH)",
|
||||
"ecdsa-sha2-nistp384": "ECDSA P-384 (SSH)",
|
||||
"ecdsa-sha2-nistp521": "ECDSA P-521 (SSH)",
|
||||
"ssh-dss": "DSA (SSH)",
|
||||
"rsa-sha2-256": "RSA-SHA2-256",
|
||||
"rsa-sha2-512": "RSA-SHA2-512",
|
||||
"ssh-rsa": t("credentials.keyTypeRSA"),
|
||||
"ssh-ed25519": t("credentials.keyTypeEd25519"),
|
||||
"ecdsa-sha2-nistp256": t("credentials.keyTypeEcdsaP256"),
|
||||
"ecdsa-sha2-nistp384": t("credentials.keyTypeEcdsaP384"),
|
||||
"ecdsa-sha2-nistp521": t("credentials.keyTypeEcdsaP521"),
|
||||
"ssh-dss": t("credentials.keyTypeDsa"),
|
||||
"rsa-sha2-256": t("credentials.keyTypeRsaSha256"),
|
||||
"rsa-sha2-512": t("credentials.keyTypeRsaSha512"),
|
||||
invalid: t("credentials.invalidKey"),
|
||||
error: t("credentials.detectionError"),
|
||||
unknown: t("credentials.unknown"),
|
||||
|
||||
@@ -141,11 +141,11 @@ export function CredentialsManager({
|
||||
|
||||
const handleDeploy = (credential: Credential) => {
|
||||
if (credential.authType !== "key") {
|
||||
toast.error("Only SSH key-based credentials can be deployed");
|
||||
toast.error(t("credentials.keyBasedOnlyForDeployment"));
|
||||
return;
|
||||
}
|
||||
if (!credential.publicKey) {
|
||||
toast.error("Public key is required for deployment");
|
||||
toast.error(t("credentials.publicKeyRequiredForDeployment"));
|
||||
return;
|
||||
}
|
||||
setDeployingCredential(credential);
|
||||
@@ -156,7 +156,7 @@ export function CredentialsManager({
|
||||
|
||||
const performDeploy = async () => {
|
||||
if (!deployingCredential || !selectedHostId) {
|
||||
toast.error("Please select a target host");
|
||||
toast.error(t("credentials.selectTargetHost"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,11 +173,11 @@ export function CredentialsManager({
|
||||
setDeployingCredential(null);
|
||||
setSelectedHostId("");
|
||||
} else {
|
||||
toast.error(result.error || "Deployment failed");
|
||||
toast.error(result.error || t("credentials.deploymentFailed"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Deployment error:", error);
|
||||
toast.error("Failed to deploy SSH key");
|
||||
toast.error(t("credentials.failedToDeployKey"));
|
||||
} finally {
|
||||
setDeployLoading(false);
|
||||
}
|
||||
@@ -564,7 +564,7 @@ export function CredentialsManager({
|
||||
}}
|
||||
title={
|
||||
folder !== t("credentials.uncategorized")
|
||||
? "Click to rename folder"
|
||||
? t("credentials.clickToRenameFolder")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
@@ -579,7 +579,7 @@ export function CredentialsManager({
|
||||
startFolderEdit(folder);
|
||||
}}
|
||||
className="h-4 w-4 p-0 opacity-50 hover:opacity-100 transition-opacity"
|
||||
title="Rename folder"
|
||||
title={t("credentials.renameFolder")}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -622,7 +622,7 @@ export function CredentialsManager({
|
||||
{credential.username}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
ID: {credential.id}
|
||||
{t("credentials.idLabel")} {credential.id}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{credential.authType === "password"
|
||||
@@ -867,7 +867,7 @@ export function CredentialsManager({
|
||||
{t("credentials.keyType")}
|
||||
</div>
|
||||
<div className="text-sm font-medium">
|
||||
{deployingCredential.keyType || "SSH Key"}
|
||||
{deployingCredential.keyType || t("credentials.sshKey")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,8 +253,7 @@ export function DockerManager({
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Docker is not enabled for this host. Enable it in Host Settings
|
||||
to use Docker features.
|
||||
{t("docker.notEnabled")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -284,8 +283,8 @@ export function DockerManager({
|
||||
<SimpleLoader size="lg" />
|
||||
<p className="text-gray-400 mt-4">
|
||||
{isValidating
|
||||
? "Validating Docker..."
|
||||
: "Connecting to host..."}
|
||||
? t("docker.validating")
|
||||
: t("docker.connectingToHost")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,11 +313,11 @@ export function DockerManager({
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<div className="font-semibold mb-2">Docker Error</div>
|
||||
<div className="font-semibold mb-2">{t("docker.error")}</div>
|
||||
<div>{dockerValidation.error}</div>
|
||||
{dockerValidation.code && (
|
||||
<div className="mt-2 text-xs opacity-70">
|
||||
Error code: {dockerValidation.code}
|
||||
{t("docker.errorCode", { code: dockerValidation.code })}
|
||||
</div>
|
||||
)}
|
||||
</AlertDescription>
|
||||
@@ -340,7 +339,7 @@ export function DockerManager({
|
||||
</h1>
|
||||
{dockerValidation?.version && (
|
||||
<p className="text-xs text-gray-400">
|
||||
Docker v{dockerValidation.version}
|
||||
{t("docker.version", { version: dockerValidation.version })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { toast } from "sonner";
|
||||
import type { SSHHost } from "@/types/index.js";
|
||||
import { getCookie, isElectron } from "@/ui/main-axios.ts";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ConsoleTerminalProps {
|
||||
sessionId: string;
|
||||
@@ -33,6 +34,7 @@ export function ConsoleTerminal({
|
||||
containerState,
|
||||
hostConfig,
|
||||
}: ConsoleTerminalProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { instance: terminal, ref: xtermRef } = useXTerm();
|
||||
const [isConnected, setIsConnected] = React.useState(false);
|
||||
const [isConnecting, setIsConnecting] = React.useState(false);
|
||||
@@ -150,7 +152,7 @@ export function ConsoleTerminal({
|
||||
if (terminal) {
|
||||
try {
|
||||
terminal.clear();
|
||||
terminal.write("Disconnected from container console.\r\n");
|
||||
terminal.write(`${t("docker.disconnectedFromContainer")}\r\n`);
|
||||
} catch (error) {
|
||||
// Terminal might be disposed
|
||||
}
|
||||
@@ -159,7 +161,7 @@ export function ConsoleTerminal({
|
||||
|
||||
const connect = React.useCallback(() => {
|
||||
if (!terminal || containerState !== "running") {
|
||||
toast.error("Container must be running to connect to console");
|
||||
toast.error(t("docker.containerMustBeRunning"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,7 +172,7 @@ export function ConsoleTerminal({
|
||||
? localStorage.getItem("jwt")
|
||||
: getCookie("jwt");
|
||||
if (!token) {
|
||||
toast.error("Authentication required");
|
||||
toast.error(t("docker.authenticationRequired"));
|
||||
setIsConnecting(false);
|
||||
return;
|
||||
}
|
||||
@@ -214,7 +216,7 @@ export function ConsoleTerminal({
|
||||
case "connected":
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false);
|
||||
toast.success(`Connected to ${containerName}`);
|
||||
toast.success(t("docker.connectedTo", { containerName }));
|
||||
|
||||
// Fit terminal and send resize to ensure correct dimensions
|
||||
setTimeout(() => {
|
||||
@@ -238,7 +240,7 @@ export function ConsoleTerminal({
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false);
|
||||
terminal.write(
|
||||
`\r\n\x1b[1;33m${msg.message || "Disconnected"}\x1b[0m\r\n`,
|
||||
`\r\n\x1b[1;33m${msg.message || t("docker.disconnected")}\x1b[0m\r\n`,
|
||||
);
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
@@ -248,8 +250,8 @@ export function ConsoleTerminal({
|
||||
|
||||
case "error":
|
||||
setIsConnecting(false);
|
||||
toast.error(msg.message || "Console error");
|
||||
terminal.write(`\r\n\x1b[1;31mError: ${msg.message}\x1b[0m\r\n`);
|
||||
toast.error(msg.message || t("docker.consoleError"));
|
||||
terminal.write(`\r\n\x1b[1;31m${t("docker.errorMessage", { message: msg.message })}\x1b[0m\r\n`);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -261,7 +263,7 @@ export function ConsoleTerminal({
|
||||
console.error("WebSocket error:", error);
|
||||
setIsConnecting(false);
|
||||
setIsConnected(false);
|
||||
toast.error("Failed to connect to console");
|
||||
toast.error(t("docker.failedToConnect"));
|
||||
};
|
||||
|
||||
// Set up periodic ping to keep connection alive
|
||||
@@ -340,9 +342,9 @@ export function ConsoleTerminal({
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-2">
|
||||
<TerminalIcon className="h-12 w-12 text-gray-600 mx-auto" />
|
||||
<p className="text-gray-400 text-lg">Container is not running</p>
|
||||
<p className="text-gray-400 text-lg">{t("docker.containerNotRunning")}</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Start the container to access the console
|
||||
{t("docker.startContainerToAccess")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -357,7 +359,7 @@ export function ConsoleTerminal({
|
||||
<div className="flex flex-col sm:flex-row gap-2 items-center sm:items-center">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<TerminalIcon className="h-5 w-5" />
|
||||
<span className="text-base font-medium">Console</span>
|
||||
<span className="text-base font-medium">{t("docker.console")}</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedShell}
|
||||
@@ -365,12 +367,12 @@ export function ConsoleTerminal({
|
||||
disabled={isConnected}
|
||||
>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="Select shell" />
|
||||
<SelectValue placeholder={t("docker.selectShell")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="bash">Bash</SelectItem>
|
||||
<SelectItem value="sh">Sh</SelectItem>
|
||||
<SelectItem value="ash">Ash</SelectItem>
|
||||
<SelectItem value="bash">{t("docker.bash")}</SelectItem>
|
||||
<SelectItem value="sh">{t("docker.sh")}</SelectItem>
|
||||
<SelectItem value="ash">{t("docker.ash")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex gap-2 sm:gap-2">
|
||||
@@ -383,12 +385,12 @@ export function ConsoleTerminal({
|
||||
{isConnecting ? (
|
||||
<>
|
||||
<div className="h-4 w-4 mr-2 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
|
||||
Connecting...
|
||||
{t("docker.connecting")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Power className="h-4 w-4 mr-2" />
|
||||
Connect
|
||||
{t("docker.connect")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -399,7 +401,7 @@ export function ConsoleTerminal({
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
<PowerOff className="h-4 w-4 mr-2" />
|
||||
Disconnect
|
||||
{t("docker.disconnect")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -422,9 +424,9 @@ export function ConsoleTerminal({
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center space-y-2">
|
||||
<TerminalIcon className="h-12 w-12 text-gray-600 mx-auto" />
|
||||
<p className="text-gray-400">Not connected</p>
|
||||
<p className="text-gray-400">{t("docker.notConnected")}</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Click Connect to start an interactive shell
|
||||
{t("docker.clickToConnect")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -436,7 +438,7 @@ export function ConsoleTerminal({
|
||||
<div className="text-center">
|
||||
<SimpleLoader size="lg" />
|
||||
<p className="text-gray-400 mt-4">
|
||||
Connecting to {containerName}...
|
||||
{t("docker.connectingTo", { containerName })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,11 +116,13 @@ export function ContainerCard({
|
||||
setIsStarting(true);
|
||||
try {
|
||||
await startDockerContainer(sessionId, container.id);
|
||||
toast.success(`Container ${container.name} started`);
|
||||
toast.success(t("docker.containerStarted", { name: container.name }));
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to start container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
t("docker.failedToStartContainer", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsStarting(false);
|
||||
@@ -132,11 +134,13 @@ export function ContainerCard({
|
||||
setIsStopping(true);
|
||||
try {
|
||||
await stopDockerContainer(sessionId, container.id);
|
||||
toast.success(`Container ${container.name} stopped`);
|
||||
toast.success(t("docker.containerStopped", { name: container.name }));
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to stop container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
t("docker.failedToStopContainer", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsStopping(false);
|
||||
@@ -148,11 +152,13 @@ export function ContainerCard({
|
||||
setIsRestarting(true);
|
||||
try {
|
||||
await restartDockerContainer(sessionId, container.id);
|
||||
toast.success(`Container ${container.name} restarted`);
|
||||
toast.success(t("docker.containerRestarted", { name: container.name }));
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to restart container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
t("docker.failedToRestartContainer", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsRestarting(false);
|
||||
@@ -165,15 +171,18 @@ export function ContainerCard({
|
||||
try {
|
||||
if (container.state === "paused") {
|
||||
await unpauseDockerContainer(sessionId, container.id);
|
||||
toast.success(`Container ${container.name} unpaused`);
|
||||
toast.success(t("docker.containerUnpaused", { name: container.name }));
|
||||
} else {
|
||||
await pauseDockerContainer(sessionId, container.id);
|
||||
toast.success(`Container ${container.name} paused`);
|
||||
toast.success(t("docker.containerPaused", { name: container.name }));
|
||||
}
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to ${container.state === "paused" ? "unpause" : "pause"} container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
t("docker.failedToTogglePauseContainer", {
|
||||
action: container.state === "paused" ? "unpause" : "pause",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsPausing(false);
|
||||
@@ -185,12 +194,14 @@ export function ContainerCard({
|
||||
try {
|
||||
const force = container.state === "running";
|
||||
await removeDockerContainer(sessionId, container.id, force);
|
||||
toast.success(`Container ${container.name} removed`);
|
||||
toast.success(t("docker.containerRemoved", { name: container.name }));
|
||||
setShowRemoveDialog(false);
|
||||
onRefresh?.();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
`Failed to remove container: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
t("docker.failedToRemoveContainer", {
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
setIsRemoving(false);
|
||||
@@ -249,21 +260,19 @@ export function ContainerCard({
|
||||
<CardContent className="space-y-3 px-4 pb-3">
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400 min-w-[50px] text-xs">Image:</span>
|
||||
<span className="text-gray-400 min-w-[50px] text-xs">{t("docker.image")}</span>
|
||||
<span className="truncate text-gray-200 text-xs">
|
||||
{container.image}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400 min-w-[50px] text-xs">ID:</span>
|
||||
<span className="text-gray-400 min-w-[50px] text-xs">{t("docker.idLabel")}</span>
|
||||
<span className="font-mono text-xs text-gray-200">
|
||||
{container.id.substring(0, 12)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-gray-400 min-w-[50px] text-xs shrink-0">
|
||||
Ports:
|
||||
</span>
|
||||
<span className="text-gray-400 min-w-[50px] text-xs shrink-0">{t("docker.ports")}</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{portsList.length > 0 ? (
|
||||
portsList.map((port, idx) => (
|
||||
@@ -280,15 +289,13 @@ export function ContainerCard({
|
||||
variant="outline"
|
||||
className="text-xs bg-gray-500/10 text-gray-400 border-gray-500/30"
|
||||
>
|
||||
None
|
||||
{t("docker.noPorts")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-400 min-w-[50px] text-xs">
|
||||
Created:
|
||||
</span>
|
||||
<span className="text-gray-400 min-w-[50px] text-xs">{t("docker.created")}</span>
|
||||
<span className="text-gray-200 text-xs">
|
||||
{formatCreatedDate(container.created)}
|
||||
</span>
|
||||
@@ -314,7 +321,7 @@ export function ContainerCard({
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Start</TooltipContent>
|
||||
<TooltipContent>{t("docker.start")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -335,7 +342,7 @@ export function ContainerCard({
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Stop</TooltipContent>
|
||||
<TooltipContent>{t("docker.stop")}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -360,7 +367,9 @@ export function ContainerCard({
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{container.state === "paused" ? "Unpause" : "Pause"}
|
||||
{container.state === "paused"
|
||||
? t("docker.unpause")
|
||||
: t("docker.pause")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -381,8 +390,7 @@ export function ContainerCard({
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Restart</TooltipContent>
|
||||
</Tooltip>
|
||||
<TooltipContent>{t("docker.restart")}</TooltipContent> </Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -399,8 +407,7 @@ export function ContainerCard({
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remove</TooltipContent>
|
||||
</Tooltip>
|
||||
<TooltipContent>{t("docker.remove")}</TooltipContent> </Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -409,25 +416,22 @@ export function ContainerCard({
|
||||
<AlertDialog open={showRemoveDialog} onOpenChange={setShowRemoveDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove Container</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("docker.removeContainer")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to remove container{" "}
|
||||
<span className="font-semibold">
|
||||
{container.name.startsWith("/")
|
||||
{t("docker.confirmRemoveContainer", {
|
||||
name: container.name.startsWith("/")
|
||||
? container.name.slice(1)
|
||||
: container.name}
|
||||
</span>
|
||||
?
|
||||
: container.name,
|
||||
})}
|
||||
{container.state === "running" && (
|
||||
<div className="mt-2 text-yellow-400">
|
||||
Warning: This container is currently running and will be
|
||||
force-removed.
|
||||
{t("docker.runningContainerWarning")}
|
||||
</div>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isRemoving}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={isRemoving}>{t("common.cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -436,7 +440,7 @@ export function ContainerCard({
|
||||
disabled={isRemoving}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
{isRemoving ? t("docker.removing") : t("common.remove")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -36,10 +36,10 @@ export function ContainerDetail({
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-gray-400 text-lg">Container not found</p>
|
||||
<p className="text-gray-400 text-lg">{t("docker.containerNotFound")}</p>
|
||||
<Button onClick={onBack} variant="outline">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back to list
|
||||
{t("docker.backToList")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,7 +52,7 @@ export function ContainerDetail({
|
||||
<div className="flex items-center gap-4 px-4 pt-3 pb-3">
|
||||
<Button variant="ghost" onClick={onBack} size="sm">
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="font-bold text-lg truncate">{container.name}</h2>
|
||||
@@ -70,9 +70,9 @@ export function ContainerDetail({
|
||||
>
|
||||
<div className="px-4 pt-2">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||
<TabsTrigger value="stats">Stats</TabsTrigger>
|
||||
<TabsTrigger value="console">Console</TabsTrigger>
|
||||
<TabsTrigger value="logs">{t("docker.logs")}</TabsTrigger>
|
||||
<TabsTrigger value="stats">{t("docker.stats")}</TabsTrigger>
|
||||
<TabsTrigger value="console">{t("docker.consoleTab")}</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,10 +55,8 @@ export function ContainerList({
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-gray-400 text-lg">No containers found</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Start by creating containers on your server
|
||||
</p>
|
||||
<p className="text-gray-400 text-lg">{t("docker.noContainersFound")}</p>
|
||||
<p className="text-gray-500 text-sm">{t("docker.noContainersFoundHint")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -71,7 +69,7 @@ export function ContainerList({
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
placeholder="Search by name, image, or ID..."
|
||||
placeholder={t("docker.searchPlaceholder")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
@@ -81,13 +79,13 @@ export function ContainerList({
|
||||
<Filter className="h-4 w-4 text-gray-400" />
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
<SelectValue placeholder={t("docker.filterByStatusPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All ({containers.length})</SelectItem>
|
||||
<SelectItem value="all">{t("docker.allContainersCount", { count: containers.length })}</SelectItem>
|
||||
{Object.entries(statusCounts).map(([status, count]) => (
|
||||
<SelectItem key={status} value={status}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)} ({count})
|
||||
{t("docker.statusCount", { status: status.charAt(0).toUpperCase() + status.slice(1), count })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -99,10 +97,8 @@ export function ContainerList({
|
||||
{filteredContainers.length === 0 ? (
|
||||
<div className="flex items-center justify-center flex-1">
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-gray-400">No containers match your filters</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Try adjusting your search or filter
|
||||
</p>
|
||||
<p className="text-gray-400">{t("docker.noContainersMatchFilters")}</p>
|
||||
<p className="text-gray-500 text-sm">{t("docker.noContainersMatchFiltersHint")}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Cpu, MemoryStick, Network, HardDrive, Activity } from "lucide-react";
|
||||
import type { DockerStats } from "@/types/index.js";
|
||||
import { getContainerStats } from "@/ui/main-axios.ts";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ContainerStatsProps {
|
||||
sessionId: string;
|
||||
@@ -24,13 +25,14 @@ export function ContainerStats({
|
||||
containerName,
|
||||
containerState,
|
||||
}: ContainerStatsProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [stats, setStats] = React.useState<DockerStats | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const fetchStats = React.useCallback(async () => {
|
||||
if (containerState !== "running") {
|
||||
setError("Container must be running to view stats");
|
||||
setError(t("docker.containerMustBeRunningToViewStats"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ export function ContainerStats({
|
||||
const data = await getContainerStats(sessionId, containerId);
|
||||
setStats(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch stats");
|
||||
setError(err instanceof Error ? err.message : t("docker.failedToFetchStats"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -60,9 +62,11 @@ export function ContainerStats({
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-2">
|
||||
<Activity className="h-12 w-12 text-gray-600 mx-auto" />
|
||||
<p className="text-gray-400 text-lg">Container is not running</p>
|
||||
<p className="text-gray-400 text-lg">
|
||||
{t("docker.containerNotRunning")}
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm">
|
||||
Start the container to view statistics
|
||||
{t("docker.startContainerToViewStats")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,7 +78,7 @@ export function ContainerStats({
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<SimpleLoader size="lg" />
|
||||
<p className="text-gray-400 mt-4">Loading stats...</p>
|
||||
<p className="text-gray-400 mt-4">{t("docker.loadingStats")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -84,7 +88,9 @@ export function ContainerStats({
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center space-y-2">
|
||||
<p className="text-red-400 text-lg">Error loading stats</p>
|
||||
<p className="text-red-400 text-lg">
|
||||
{t("docker.errorLoadingStats")}
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -94,7 +100,7 @@ export function ContainerStats({
|
||||
if (!stats) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-gray-400">No stats available</p>
|
||||
<p className="text-gray-400">{t("docker.noStatsAvailable")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -109,14 +115,13 @@ export function ContainerStats({
|
||||
<CardHeader className="pb-2 px-4">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Cpu className="h-5 w-5 text-blue-400" />
|
||||
CPU Usage
|
||||
{t("docker.cpuUsage")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Current</span>
|
||||
<span className="font-mono font-semibold text-blue-300">
|
||||
<span className="text-gray-400">{t("docker.current")}</span> <span className="font-mono font-semibold text-blue-300">
|
||||
{stats.cpu}
|
||||
</span>
|
||||
</div>
|
||||
@@ -130,19 +135,19 @@ export function ContainerStats({
|
||||
<CardHeader className="pb-2 px-4">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<MemoryStick className="h-5 w-5 text-purple-400" />
|
||||
Memory Usage
|
||||
{t("docker.memoryUsage")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Used / Limit</span>
|
||||
<span className="text-gray-400">{t("docker.usedLimit")}</span>
|
||||
<span className="font-mono font-semibold text-purple-300">
|
||||
{stats.memoryUsed} / {stats.memoryLimit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Percentage</span>
|
||||
<span className="text-gray-400">{t("docker.percentage")}</span>
|
||||
<span className="font-mono text-purple-300">
|
||||
{stats.memoryPercent}
|
||||
</span>
|
||||
@@ -157,17 +162,17 @@ export function ContainerStats({
|
||||
<CardHeader className="pb-2 px-4">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Network className="h-5 w-5 text-green-400" />
|
||||
Network I/O
|
||||
{t("docker.networkIo")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Input</span>
|
||||
<span className="text-gray-400">{t("docker.input")}</span>
|
||||
<span className="font-mono text-green-300">{stats.netInput}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Output</span>
|
||||
<span className="text-gray-400">{t("docker.output")}</span>
|
||||
<span className="font-mono text-green-300">
|
||||
{stats.netOutput}
|
||||
</span>
|
||||
@@ -181,26 +186,26 @@ export function ContainerStats({
|
||||
<CardHeader className="pb-2 px-4">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5 text-orange-400" />
|
||||
Block I/O
|
||||
{t("docker.blockIo")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Read</span>
|
||||
<span className="text-gray-400">{t("docker.read")}</span>
|
||||
<span className="font-mono text-orange-300">
|
||||
{stats.blockRead}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">Write</span>
|
||||
<span className="text-gray-400">{t("docker.write")}</span>
|
||||
<span className="font-mono text-orange-300">
|
||||
{stats.blockWrite}
|
||||
</span>
|
||||
</div>
|
||||
{stats.pids && (
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-gray-400">PIDs</span>
|
||||
<span className="text-gray-400">{t("docker.pids")}</span>
|
||||
<span className="font-mono text-orange-300">{stats.pids}</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -213,23 +218,23 @@ export function ContainerStats({
|
||||
<CardHeader className="pb-2 px-4">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<Activity className="h-5 w-5 text-cyan-400" />
|
||||
Container Information
|
||||
{t("docker.containerInformation")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">Name:</span>
|
||||
<span className="text-gray-400">{t("docker.name")}</span>
|
||||
<span className="font-mono text-gray-200">{containerName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">ID:</span>
|
||||
<span className="text-gray-400">{t("docker.id")}</span>
|
||||
<span className="font-mono text-sm text-gray-300">
|
||||
{containerId.substring(0, 12)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">State:</span>
|
||||
<span className="text-gray-400">{t("docker.state")}</span>
|
||||
<span className="font-semibold text-green-400 capitalize">
|
||||
{containerState}
|
||||
</span>
|
||||
|
||||
@@ -348,7 +348,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
if (result?.requires_totp) {
|
||||
setTotpRequired(true);
|
||||
setTotpSessionId(sessionId);
|
||||
setTotpPrompt(result.prompt || "Verification code:");
|
||||
setTotpPrompt(result.prompt || t("fileManager.verificationCodePrompt"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -586,7 +586,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
t("fileManager.sshConnectionFailed", { name: currentHost?.name, ip: currentHost?.ip, port: currentHost?.port }),
|
||||
);
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToUploadFile"));
|
||||
@@ -633,7 +633,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`,
|
||||
t("fileManager.sshConnectionFailed", { name: currentHost?.name, ip: currentHost?.ip, port: currentHost?.port }),
|
||||
);
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToDownloadFile"));
|
||||
@@ -1497,7 +1497,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
if (result?.requires_totp) {
|
||||
setTotpRequired(true);
|
||||
setTotpSessionId(sessionId);
|
||||
setTotpPrompt(result.prompt || "Verification code:");
|
||||
setTotpPrompt(result.prompt || t("fileManager.verificationCodePrompt"));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1166,7 +1166,7 @@ export function HostManagerEditor({
|
||||
<TabsTrigger value="terminal">
|
||||
{t("hosts.terminal")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||
<TabsTrigger value="docker">{t("hosts.docker")}</TabsTrigger>
|
||||
<TabsTrigger value="tunnel">{t("hosts.tunnel")}</TabsTrigger>
|
||||
<TabsTrigger value="file_manager">
|
||||
{t("hosts.fileManager")}
|
||||
@@ -1895,7 +1895,7 @@ export function HostManagerEditor({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="proxy.example.com"
|
||||
placeholder={t("placeholders.socks5Host")}
|
||||
{...field}
|
||||
onBlur={(e) => {
|
||||
field.onChange(
|
||||
@@ -1923,7 +1923,7 @@ export function HostManagerEditor({
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="1080"
|
||||
placeholder={t("placeholders.socks5Port")}
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
@@ -1945,8 +1945,7 @@ export function HostManagerEditor({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("hosts.socks5Username")} (
|
||||
{t("hosts.optional")})
|
||||
{t("hosts.socks5Username")} {t("hosts.optional")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -1970,8 +1969,7 @@ export function HostManagerEditor({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("hosts.socks5Password")} (
|
||||
{t("hosts.optional")})
|
||||
{t("hosts.socks5Password")} {t("hosts.optional")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput
|
||||
@@ -2059,7 +2057,7 @@ export function HostManagerEditor({
|
||||
{t("hosts.socks5Host")}
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder="proxy.example.com"
|
||||
placeholder={t("placeholders.socks5Host")}
|
||||
value={node.host}
|
||||
onChange={(e) => {
|
||||
const currentChain =
|
||||
@@ -2104,7 +2102,7 @@ export function HostManagerEditor({
|
||||
</FormLabel>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="1080"
|
||||
placeholder={t("placeholders.socks5Port")}
|
||||
value={node.port}
|
||||
onChange={(e) => {
|
||||
const currentChain =
|
||||
@@ -2155,10 +2153,10 @@ export function HostManagerEditor({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="4">
|
||||
SOCKS4
|
||||
{t("hosts.socks4")}
|
||||
</SelectItem>
|
||||
<SelectItem value="5">
|
||||
SOCKS5
|
||||
{t("hosts.socks5")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -2167,8 +2165,7 @@ export function HostManagerEditor({
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
{t("hosts.socks5Username")} (
|
||||
{t("hosts.optional")})
|
||||
{t("hosts.socks5Username")} {t("hosts.optional")}
|
||||
</FormLabel>
|
||||
<Input
|
||||
placeholder={t("hosts.username")}
|
||||
@@ -2212,8 +2209,7 @@ export function HostManagerEditor({
|
||||
|
||||
<div className="space-y-2">
|
||||
<FormLabel>
|
||||
{t("hosts.socks5Password")} (
|
||||
{t("hosts.optional")})
|
||||
{t("hosts.socks5Password")} {t("hosts.optional")}
|
||||
</FormLabel>
|
||||
<PasswordInput
|
||||
placeholder={t("hosts.password")}
|
||||
@@ -2720,7 +2716,7 @@ export function HostManagerEditor({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Execute a snippet when the terminal connects
|
||||
{t("hosts.executeSnippetOnConnect")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
);
|
||||
@@ -2733,9 +2729,9 @@ export function HostManagerEditor({
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Auto-MOSH</FormLabel>
|
||||
<FormLabel>{t("hosts.autoMosh")}</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically run MOSH command on connect
|
||||
{t("hosts.autoMoshDesc")}
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
@@ -2754,11 +2750,10 @@ export function HostManagerEditor({
|
||||
name="terminalConfig.moshCommand"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>MOSH Command</FormLabel>
|
||||
<FormLabel>{t("hosts.moshCommand")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="mosh user@server"
|
||||
{...field}
|
||||
placeholder={t("placeholders.moshCommand")} {...field}
|
||||
onBlur={(e) => {
|
||||
field.onChange(e.target.value.trim());
|
||||
field.onBlur();
|
||||
@@ -2766,7 +2761,7 @@ export function HostManagerEditor({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The MOSH command to execute
|
||||
{t("hosts.moshCommandDesc")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -2819,11 +2814,10 @@ export function HostManagerEditor({
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
Environment Variables
|
||||
{t("hosts.environmentVariables")}
|
||||
</label>
|
||||
<FormDescription>
|
||||
Set custom environment variables for the terminal
|
||||
session
|
||||
{t("hosts.environmentVariablesDesc")}
|
||||
</FormDescription>
|
||||
{form
|
||||
.watch("terminalConfig.environmentVariables")
|
||||
@@ -2836,7 +2830,7 @@ export function HostManagerEditor({
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Variable name"
|
||||
placeholder={t("hosts.variableName")}
|
||||
{...field}
|
||||
onBlur={(e) => {
|
||||
field.onChange(
|
||||
@@ -2856,7 +2850,7 @@ export function HostManagerEditor({
|
||||
<FormItem className="flex-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Value"
|
||||
placeholder={t("hosts.variableValue")}
|
||||
{...field}
|
||||
onBlur={(e) => {
|
||||
field.onChange(
|
||||
@@ -2903,7 +2897,7 @@ export function HostManagerEditor({
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Variable
|
||||
{t("hosts.addVariable")}
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
@@ -2916,7 +2910,7 @@ export function HostManagerEditor({
|
||||
name="enableDocker"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enable Docker</FormLabel>
|
||||
<FormLabel>{t("hosts.enableDocker")}</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
@@ -2924,7 +2918,7 @@ export function HostManagerEditor({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Enable Docker integration for this host
|
||||
{t("hosts.enableDockerDesc")}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -3075,7 +3069,7 @@ export function HostManagerEditor({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="22"
|
||||
placeholder={t("placeholders.defaultPort")}
|
||||
{...sourcePortField}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -3094,7 +3088,7 @@ export function HostManagerEditor({
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="224"
|
||||
placeholder={t("placeholders.defaultEndpointPort")}
|
||||
{...endpointPortField}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -381,7 +381,9 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success(
|
||||
`Exported host configuration for ${host.name || host.username}@${host.ip}`,
|
||||
t("hosts.exportedHostConfig", {
|
||||
name: host.name || `${host.username}@${host.ip}`,
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
toast.error(t("hosts.failedToExportHost"));
|
||||
@@ -1072,7 +1074,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
}}
|
||||
title={
|
||||
folder !== t("hosts.uncategorized")
|
||||
? "Click to rename folder"
|
||||
? t("hosts.clickToRenameFolder")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
@@ -1087,7 +1089,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
startFolderEdit(folder);
|
||||
}}
|
||||
className="h-4 w-4 p-0 opacity-50 hover:opacity-100 transition-opacity"
|
||||
title="Rename folder"
|
||||
title={t("hosts.renameFolder")}
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</Button>
|
||||
@@ -1234,7 +1236,9 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Remove from folder "{host.folder}"
|
||||
{t("hosts.removeFromFolder", {
|
||||
folder: host.folder,
|
||||
})}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -1254,7 +1258,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Edit host</p>
|
||||
<p>{t("hosts.editHostTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -1276,7 +1280,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Delete host</p>
|
||||
<p>{t("hosts.deleteHostTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -1294,7 +1298,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Export host</p>
|
||||
<p>{t("hosts.exportHostTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
@@ -1312,7 +1316,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Clone host</p>
|
||||
<p>{t("hosts.cloneHostTooltip")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -1384,7 +1388,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
className="text-xs px-1 py-0"
|
||||
>
|
||||
<Container className="h-2 w-2 mr-0.5" />
|
||||
Docker
|
||||
{t("hosts.docker")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -1414,7 +1418,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open Terminal</p>
|
||||
<p>{t("hosts.openTerminal")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -1495,7 +1499,7 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open Docker</p>
|
||||
<p>{t("hosts.openDocker")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -1530,10 +1534,10 @@ export function HostManagerViewer({ onEditHost }: SSHManagerHostViewerProps) {
|
||||
<TooltipContent>
|
||||
<div className="text-center">
|
||||
<p className="font-medium">
|
||||
Click to edit host
|
||||
{t("hosts.clickToEditHost")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Drag to move between folders
|
||||
{t("hosts.dragToMoveBetweenFolders")}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
|
||||
@@ -684,9 +684,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
!sudoPromptShownRef.current
|
||||
) {
|
||||
sudoPromptShownRef.current = true;
|
||||
confirmWithToast(
|
||||
t("terminal.sudoPasswordPopupTitle", "Insert password?"),
|
||||
async () => {
|
||||
confirmWithToast(t("terminal.sudoPasswordPopupTitle"), async () => {
|
||||
if (
|
||||
webSocketRef.current &&
|
||||
webSocketRef.current.readyState === WebSocket.OPEN
|
||||
@@ -843,7 +841,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
} else if (msg.type === "totp_required") {
|
||||
setTotpRequired(true);
|
||||
setTotpPrompt(msg.prompt || "Verification code:");
|
||||
setTotpPrompt(msg.prompt || t("terminal.totpCodeLabel"));
|
||||
setIsPasswordPrompt(false);
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
@@ -851,7 +849,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
} else if (msg.type === "password_required") {
|
||||
setTotpRequired(true);
|
||||
setTotpPrompt(msg.prompt || "Password:");
|
||||
setTotpPrompt(msg.prompt || t("common.password"));
|
||||
setIsPasswordPrompt(true);
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
|
||||
@@ -283,9 +283,9 @@ export function SSHToolsSidebar({
|
||||
console.error("Failed to fetch command history", err);
|
||||
const errorMessage =
|
||||
err?.response?.status === 401
|
||||
? "Authentication required. Please refresh the page."
|
||||
? t("commandHistory.authRequiredRefresh")
|
||||
: err?.response?.status === 403
|
||||
? "Data access locked. Please re-authenticate."
|
||||
? t("commandHistory.dataAccessLockedReauth")
|
||||
: err?.message || "Failed to load command history";
|
||||
|
||||
setHistoryError(errorMessage);
|
||||
@@ -814,9 +814,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
if (sourceFolder !== targetFolder) {
|
||||
toast.error(
|
||||
t("snippets.reorderSameFolder", {
|
||||
defaultValue: "Can only reorder snippets within the same folder",
|
||||
}),
|
||||
t("snippets.reorderSameFolder"),
|
||||
);
|
||||
setDraggedSnippet(null);
|
||||
setDragOverFolder(null);
|
||||
@@ -853,16 +851,12 @@ export function SSHToolsSidebar({
|
||||
try {
|
||||
await reorderSnippets(updates);
|
||||
toast.success(
|
||||
t("snippets.reorderSuccess", {
|
||||
defaultValue: "Snippets reordered successfully",
|
||||
}),
|
||||
t("snippets.reorderSuccess"),
|
||||
);
|
||||
fetchSnippets();
|
||||
} catch {
|
||||
toast.error(
|
||||
t("snippets.reorderFailed", {
|
||||
defaultValue: "Failed to reorder snippets",
|
||||
}),
|
||||
t("snippets.reorderFailed"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -901,23 +895,13 @@ export function SSHToolsSidebar({
|
||||
confirmWithToast(
|
||||
t("snippets.deleteFolderConfirm", {
|
||||
name: folderName,
|
||||
defaultValue: `Delete folder "${folderName}"? All snippets will be moved to Uncategorized.`,
|
||||
}),
|
||||
async () => {
|
||||
try {
|
||||
await deleteSnippetFolder(folderName);
|
||||
toast.success(
|
||||
t("snippets.deleteFolderSuccess", {
|
||||
defaultValue: "Folder deleted successfully",
|
||||
}),
|
||||
);
|
||||
toast.success(t("snippets.deleteFolderSuccess"));
|
||||
fetchSnippets();
|
||||
} catch {
|
||||
toast.error(
|
||||
t("snippets.deleteFolderFailed", {
|
||||
defaultValue: "Failed to delete folder",
|
||||
}),
|
||||
);
|
||||
toast.error(t("snippets.deleteFolderFailed"));
|
||||
}
|
||||
},
|
||||
"destructive",
|
||||
@@ -944,22 +928,14 @@ export function SSHToolsSidebar({
|
||||
color: folderFormData.color || undefined,
|
||||
icon: folderFormData.icon || undefined,
|
||||
});
|
||||
toast.success(
|
||||
t("snippets.updateFolderSuccess", {
|
||||
defaultValue: "Folder updated successfully",
|
||||
}),
|
||||
);
|
||||
toast.success(t("snippets.updateFolderSuccess"));
|
||||
} else {
|
||||
await createSnippetFolder({
|
||||
name: folderFormData.name,
|
||||
color: folderFormData.color || undefined,
|
||||
icon: folderFormData.icon || undefined,
|
||||
});
|
||||
toast.success(
|
||||
t("snippets.createFolderSuccess", {
|
||||
defaultValue: "Folder created successfully",
|
||||
}),
|
||||
);
|
||||
toast.success(t("snippets.createFolderSuccess"));
|
||||
}
|
||||
|
||||
setShowFolderDialog(false);
|
||||
@@ -967,12 +943,8 @@ export function SSHToolsSidebar({
|
||||
} catch {
|
||||
toast.error(
|
||||
editingFolder
|
||||
? t("snippets.updateFolderFailed", {
|
||||
defaultValue: "Failed to update folder",
|
||||
})
|
||||
: t("snippets.createFolderFailed", {
|
||||
defaultValue: "Failed to create folder",
|
||||
}),
|
||||
? t("snippets.updateFolderFailed")
|
||||
: t("snippets.createFolderFailed"),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1042,9 +1014,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
if (splitAssignments.size === 0) {
|
||||
toast.error(
|
||||
t("splitScreen.error.noAssignments", {
|
||||
defaultValue: "Please drag tabs to cells before applying",
|
||||
}),
|
||||
t("splitScreen.error.noAssignments"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -1054,7 +1024,6 @@ export function SSHToolsSidebar({
|
||||
if (splitAssignments.size < requiredSlots) {
|
||||
toast.error(
|
||||
t("splitScreen.error.fillAllSlots", {
|
||||
defaultValue: `Please fill all ${requiredSlots} layout spots before applying`,
|
||||
count: requiredSlots,
|
||||
}),
|
||||
);
|
||||
@@ -1083,9 +1052,7 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
|
||||
toast.success(
|
||||
t("splitScreen.success", {
|
||||
defaultValue: "Split screen applied",
|
||||
}),
|
||||
t("splitScreen.success"),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1099,9 +1066,7 @@ export function SSHToolsSidebar({
|
||||
setPreviewKey((prev) => prev + 1);
|
||||
|
||||
toast.success(
|
||||
t("splitScreen.cleared", {
|
||||
defaultValue: "Split screen cleared",
|
||||
}),
|
||||
t("splitScreen.cleared"),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1121,15 +1086,11 @@ export function SSHToolsSidebar({
|
||||
await deleteCommandFromHistory(activeTerminalHostId, command);
|
||||
setCommandHistory((prev) => prev.filter((c) => c !== command));
|
||||
toast.success(
|
||||
t("commandHistory.deleteSuccess", {
|
||||
defaultValue: "Command deleted from history",
|
||||
}),
|
||||
t("commandHistory.deleteSuccess"),
|
||||
);
|
||||
} catch {
|
||||
toast.error(
|
||||
t("commandHistory.deleteFailed", {
|
||||
defaultValue: "Failed to delete command.",
|
||||
}),
|
||||
t("commandHistory.deleteFailed"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1159,7 +1120,7 @@ export function SSHToolsSidebar({
|
||||
variant="outline"
|
||||
onClick={() => setSidebarWidth(400)}
|
||||
className="w-[28px] h-[28px]"
|
||||
title="Reset sidebar width"
|
||||
title={t("common.resetSidebarWidth")}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -1189,10 +1150,10 @@ export function SSHToolsSidebar({
|
||||
{t("snippets.title")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="command-history">
|
||||
{t("commandHistory.title", { defaultValue: "History" })}
|
||||
{t("commandHistory.title")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="split-screen">
|
||||
{t("splitScreen.title", { defaultValue: "Split Screen" })}
|
||||
{t("splitScreen.title")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -1301,20 +1262,14 @@ export function SSHToolsSidebar({
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{t("snippets.selectTerminals", {
|
||||
defaultValue: "Select Terminals (optional)",
|
||||
})}
|
||||
{t("snippets.selectTerminals")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedSnippetTabIds.length > 0
|
||||
? t("snippets.executeOnSelected", {
|
||||
defaultValue: `Execute on ${selectedSnippetTabIds.length} selected terminal(s)`,
|
||||
count: selectedSnippetTabIds.length,
|
||||
})
|
||||
: t("snippets.executeOnCurrent", {
|
||||
defaultValue:
|
||||
"Execute on current terminal (click to select multiple)",
|
||||
})}
|
||||
: t("snippets.executeOnCurrent")}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
||||
{terminalTabs.map((tab) => (
|
||||
@@ -1354,9 +1309,7 @@ export function SSHToolsSidebar({
|
||||
variant="outline"
|
||||
>
|
||||
<FolderPlus className="w-4 h-4 mr-2" />
|
||||
{t("snippets.newFolder", {
|
||||
defaultValue: "New Folder",
|
||||
})}
|
||||
{t("snippets.newFolder")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1606,9 +1559,7 @@ export function SSHToolsSidebar({
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder={t("commandHistory.searchPlaceholder", {
|
||||
defaultValue: "Search commands...",
|
||||
})}
|
||||
placeholder={t("commandHistory.searchPlaceholder")}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
@@ -1627,10 +1578,7 @@ export function SSHToolsSidebar({
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground bg-muted/30 px-2 py-1.5 rounded">
|
||||
{t("commandHistory.tabHint", {
|
||||
defaultValue:
|
||||
"Use Tab in Terminal to autocomplete from command history",
|
||||
})}
|
||||
{t("commandHistory.tabHint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1639,9 +1587,7 @@ export function SSHToolsSidebar({
|
||||
<div className="text-center py-8">
|
||||
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4 mb-4">
|
||||
<p className="text-destructive font-medium mb-2">
|
||||
{t("commandHistory.error", {
|
||||
defaultValue: "Error loading history",
|
||||
})}
|
||||
{t("commandHistory.error")}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{historyError}
|
||||
@@ -1653,32 +1599,23 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
{t("common.retry", { defaultValue: "Retry" })}
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
) : !activeTerminal ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<Terminal className="h-12 w-12 mb-4 opacity-20 mx-auto" />
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.noTerminal", {
|
||||
defaultValue: "No active terminal",
|
||||
})}
|
||||
</p>
|
||||
{t("commandHistory.noTerminal")} </p>
|
||||
<p className="text-sm">
|
||||
{t("commandHistory.noTerminalHint", {
|
||||
defaultValue:
|
||||
"Open a terminal to see its command history.",
|
||||
})}
|
||||
{t("commandHistory.noTerminalHint")}
|
||||
</p>
|
||||
</div>
|
||||
) : isHistoryLoading && commandHistory.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
<Loader2 className="h-12 w-12 mb-4 opacity-20 mx-auto animate-spin" />
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.loading", {
|
||||
defaultValue: "Loading command history...",
|
||||
})}
|
||||
</p>
|
||||
{t("commandHistory.loading")} </p>
|
||||
</div>
|
||||
) : filteredCommands.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-8">
|
||||
@@ -1686,13 +1623,10 @@ export function SSHToolsSidebar({
|
||||
<>
|
||||
<Search className="h-12 w-12 mb-2 opacity-20 mx-auto" />
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.noResults", {
|
||||
defaultValue: "No commands found",
|
||||
})}
|
||||
{t("commandHistory.noResults")}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{t("commandHistory.noResultsHint", {
|
||||
defaultValue: `No commands matching "${searchQuery}"`,
|
||||
query: searchQuery,
|
||||
})}
|
||||
</p>
|
||||
@@ -1700,15 +1634,10 @@ export function SSHToolsSidebar({
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-2 font-medium">
|
||||
{t("commandHistory.empty", {
|
||||
defaultValue: "No command history yet",
|
||||
})}
|
||||
{t("commandHistory.empty")}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{t("commandHistory.emptyHint", {
|
||||
defaultValue:
|
||||
"Execute commands in the active terminal to build its history.",
|
||||
})}
|
||||
{t("commandHistory.emptyHint")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -1739,9 +1668,7 @@ export function SSHToolsSidebar({
|
||||
e.stopPropagation();
|
||||
handleCommandDelete(command);
|
||||
}}
|
||||
title={t("commandHistory.deleteTooltip", {
|
||||
defaultValue: "Delete command",
|
||||
})}
|
||||
title={t("commandHistory.deleteTooltip")}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
@@ -1769,23 +1696,14 @@ export function SSHToolsSidebar({
|
||||
>
|
||||
<TabsList className="w-full grid grid-cols-4">
|
||||
<TabsTrigger value="none">
|
||||
{t("splitScreen.none", { defaultValue: "None" })}
|
||||
{t("splitScreen.none")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="2">
|
||||
{t("splitScreen.twoSplit", {
|
||||
defaultValue: "2-Split",
|
||||
})}
|
||||
</TabsTrigger>
|
||||
{t("splitScreen.twoSplit")} </TabsTrigger>
|
||||
<TabsTrigger value="3">
|
||||
{t("splitScreen.threeSplit", {
|
||||
defaultValue: "3-Split",
|
||||
})}
|
||||
</TabsTrigger>
|
||||
{t("splitScreen.threeSplit")} </TabsTrigger>
|
||||
<TabsTrigger value="4">
|
||||
{t("splitScreen.fourSplit", {
|
||||
defaultValue: "4-Split",
|
||||
})}
|
||||
</TabsTrigger>
|
||||
{t("splitScreen.fourSplit")} </TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
@@ -1795,15 +1713,10 @@ export function SSHToolsSidebar({
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{t("splitScreen.availableTabs", {
|
||||
defaultValue: "Available Tabs",
|
||||
})}
|
||||
{t("splitScreen.availableTabs")}
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
{t("splitScreen.dragTabsHint", {
|
||||
defaultValue:
|
||||
"Drag tabs into the grid below to position them",
|
||||
})}
|
||||
{t("splitScreen.dragTabsHint")}
|
||||
</p>
|
||||
<div className="space-y-1 max-h-[200px] overflow-y-auto">
|
||||
{splittableTabs.map((tab) => {
|
||||
@@ -1843,9 +1756,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white">
|
||||
{t("splitScreen.layout", {
|
||||
defaultValue: "Layout",
|
||||
})}
|
||||
{t("splitScreen.layout")}
|
||||
</label>
|
||||
<div
|
||||
className={`grid gap-2 ${
|
||||
@@ -1904,14 +1815,12 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
className="h-6 text-xs hover:bg-red-500/20"
|
||||
>
|
||||
Remove
|
||||
{t("common.remove")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("splitScreen.dropHere", {
|
||||
defaultValue: "Drop tab here",
|
||||
})}
|
||||
{t("splitScreen.dropHere")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1927,18 +1836,14 @@ export function SSHToolsSidebar({
|
||||
className="flex-1"
|
||||
disabled={splitAssignments.size === 0}
|
||||
>
|
||||
{t("splitScreen.apply", {
|
||||
defaultValue: "Apply Split",
|
||||
})}
|
||||
{t("splitScreen.apply")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClearSplit}
|
||||
className="flex-1"
|
||||
>
|
||||
{t("splitScreen.clear", {
|
||||
defaultValue: "Clear",
|
||||
})}
|
||||
{t("splitScreen.clear")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -1948,16 +1853,10 @@ export function SSHToolsSidebar({
|
||||
<div className="text-center py-8">
|
||||
<LayoutGrid className="h-12 w-12 mb-4 opacity-20 mx-auto" />
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
{t("splitScreen.selectMode", {
|
||||
defaultValue:
|
||||
"Select a split mode to get started",
|
||||
})}
|
||||
{t("splitScreen.selectMode")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("splitScreen.helpText", {
|
||||
defaultValue:
|
||||
"Choose how many tabs you want to display at once",
|
||||
})}
|
||||
{t("splitScreen.helpText")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -1987,7 +1886,7 @@ export function SSHToolsSidebar({
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}
|
||||
}}
|
||||
title="Drag to resize sidebar"
|
||||
title={t("common.dragToResizeSidebar")}
|
||||
/>
|
||||
)}
|
||||
</Sidebar>
|
||||
@@ -2056,7 +1955,7 @@ export function SSHToolsSidebar({
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center gap-2">
|
||||
<Folder className="h-4 w-4" />
|
||||
{t("snippets.folder", { defaultValue: "Folder" })}
|
||||
{t("snippets.folder")}
|
||||
<span className="text-muted-foreground">
|
||||
({t("common.optional")})
|
||||
</span>
|
||||
@@ -2072,16 +1971,12 @@ export function SSHToolsSidebar({
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t("snippets.selectFolder", {
|
||||
defaultValue: "Select a folder or leave empty",
|
||||
})}
|
||||
placeholder={t("snippets.selectFolder")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__no_folder__">
|
||||
{t("snippets.noFolder", {
|
||||
defaultValue: "No folder (Uncategorized)",
|
||||
})}
|
||||
{t("snippets.noFolder")}
|
||||
</SelectItem>
|
||||
{snippetFolders.map((folder) => {
|
||||
const FolderIcon = getFolderIcon(folder.name);
|
||||
@@ -2155,26 +2050,20 @@ export function SSHToolsSidebar({
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
{editingFolder
|
||||
? t("snippets.editFolder", { defaultValue: "Edit Folder" })
|
||||
: t("snippets.createFolder", {
|
||||
defaultValue: "Create Folder",
|
||||
})}
|
||||
? t("snippets.editFolder")
|
||||
: t("snippets.createFolder")
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{editingFolder
|
||||
? t("snippets.editFolderDescription", {
|
||||
defaultValue: "Customize your snippet folder",
|
||||
})
|
||||
: t("snippets.createFolderDescription", {
|
||||
defaultValue: "Organize your snippets into folders",
|
||||
})}
|
||||
? t("snippets.editFolderDescription")
|
||||
: t("snippets.createFolderDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-white flex items-center gap-1">
|
||||
{t("snippets.folderName", { defaultValue: "Folder Name" })}
|
||||
{t("snippets.folderName")}
|
||||
<span className="text-destructive">*</span>
|
||||
</label>
|
||||
<Input
|
||||
@@ -2185,24 +2074,20 @@ export function SSHToolsSidebar({
|
||||
name: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("snippets.folderNamePlaceholder", {
|
||||
defaultValue: "e.g., System Commands, Docker Scripts",
|
||||
})}
|
||||
placeholder={t("sshTools.scripts.inputPlaceholder")}
|
||||
className={`${folderFormErrors.name ? "border-destructive focus-visible:ring-destructive" : ""}`}
|
||||
autoFocus
|
||||
/>
|
||||
{folderFormErrors.name && (
|
||||
<p className="text-xs text-destructive mt-1">
|
||||
{t("snippets.folderNameRequired", {
|
||||
defaultValue: "Folder name is required",
|
||||
})}
|
||||
{t("snippets.folderNameRequired")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-semibold text-white">
|
||||
{t("snippets.folderColor", { defaultValue: "Folder Color" })}
|
||||
{t("snippets.folderColor")}
|
||||
</Label>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{AVAILABLE_COLORS.map((color) => (
|
||||
@@ -2229,7 +2114,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-semibold text-white">
|
||||
{t("snippets.folderIcon", { defaultValue: "Folder Icon" })}
|
||||
{t("snippets.folderIcon")}
|
||||
</Label>
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
{AVAILABLE_ICONS.map(({ value, label, Icon }) => (
|
||||
@@ -2254,7 +2139,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base font-semibold text-white">
|
||||
{t("snippets.preview", { defaultValue: "Preview" })}
|
||||
{t("snippets.preview")}
|
||||
</Label>
|
||||
<div className="flex items-center gap-3 p-4 rounded-md bg-dark-bg-darker border border-dark-border">
|
||||
{(() => {
|
||||
@@ -2271,7 +2156,7 @@ export function SSHToolsSidebar({
|
||||
})()}
|
||||
<span className="font-medium">
|
||||
{folderFormData.name ||
|
||||
t("snippets.folderName", { defaultValue: "Folder Name" })}
|
||||
t("snippets.folderName")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2289,12 +2174,8 @@ export function SSHToolsSidebar({
|
||||
</Button>
|
||||
<Button onClick={handleFolderSubmit} className="flex-1">
|
||||
{editingFolder
|
||||
? t("snippets.updateFolder", {
|
||||
defaultValue: "Update Folder",
|
||||
})
|
||||
: t("snippets.createFolder", {
|
||||
defaultValue: "Create Folder",
|
||||
})}
|
||||
? t("snippets.updateFolder")
|
||||
: t("snippets.createFolder")
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -104,7 +104,7 @@ export function TunnelObject({
|
||||
default:
|
||||
return {
|
||||
icon: <WifiOff className="h-4 w-4" />,
|
||||
text: statusValue,
|
||||
text: t("tunnels.unknown"),
|
||||
color: "text-muted-foreground",
|
||||
bgColor: "bg-muted/30",
|
||||
borderColor: "border-border",
|
||||
@@ -243,7 +243,7 @@ export function TunnelObject({
|
||||
>
|
||||
{t("tunnels.discord")}
|
||||
</a>{" "}
|
||||
or create a{" "}
|
||||
{t("tunnels.orCreate")}{" "}
|
||||
<a
|
||||
href="https://github.com/Termix-SSH/Termix/issues/new"
|
||||
target="_blank"
|
||||
@@ -479,7 +479,7 @@ export function TunnelObject({
|
||||
>
|
||||
{t("tunnels.discord")}
|
||||
</a>{" "}
|
||||
or create a{" "}
|
||||
{t("tunnels.orCreate")}{" "}
|
||||
<a
|
||||
href="https://github.com/Termix-SSH/Termix/issues/new"
|
||||
target="_blank"
|
||||
|
||||
Reference in New Issue
Block a user