fix: translations
This commit is contained in:
@@ -472,6 +472,14 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
cleanupSession(sessionId);
|
cleanupSession(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up any stale pending TOTP sessions
|
||||||
|
if (pendingTOTPSessions[sessionId]) {
|
||||||
|
try {
|
||||||
|
pendingTOTPSessions[sessionId].client.end();
|
||||||
|
} catch {}
|
||||||
|
delete pendingTOTPSessions[sessionId];
|
||||||
|
}
|
||||||
|
|
||||||
let resolvedCredentials: any = {
|
let resolvedCredentials: any = {
|
||||||
password: host.password,
|
password: host.password,
|
||||||
sshKey: host.key,
|
sshKey: host.key,
|
||||||
@@ -552,9 +560,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
host: host.ip,
|
host: host.ip,
|
||||||
port: host.port || 22,
|
port: host.port || 22,
|
||||||
username: host.username,
|
username: host.username,
|
||||||
tryKeyboard:
|
tryKeyboard: true,
|
||||||
resolvedCredentials.authType === "none" ||
|
|
||||||
forceKeyboardInteractive === true,
|
|
||||||
keepaliveInterval: 30000,
|
keepaliveInterval: 30000,
|
||||||
keepaliveCountMax: 3,
|
keepaliveCountMax: 3,
|
||||||
readyTimeout: 60000,
|
readyTimeout: 60000,
|
||||||
@@ -564,7 +570,7 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
|
|
||||||
if (resolvedCredentials.authType === "none") {
|
if (resolvedCredentials.authType === "none") {
|
||||||
} else if (resolvedCredentials.authType === "password") {
|
} else if (resolvedCredentials.authType === "password") {
|
||||||
if (!forceKeyboardInteractive && resolvedCredentials.password) {
|
if (resolvedCredentials.password) {
|
||||||
config.password = resolvedCredentials.password;
|
config.password = resolvedCredentials.password;
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
@@ -689,37 +695,29 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (totpPromptIndex !== -1) {
|
if (totpPromptIndex !== -1) {
|
||||||
if (pendingTOTPSessions[sessionId]) {
|
if (responseSent) {
|
||||||
const existingSession = pendingTOTPSessions[sessionId];
|
const responses = prompts.map((p) => {
|
||||||
if (existingSession.totpAttempts >= 3) {
|
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||||
if (!responseSent) {
|
return resolvedCredentials.password;
|
||||||
responseSent = true;
|
|
||||||
delete pendingTOTPSessions[sessionId];
|
|
||||||
client.end();
|
|
||||||
res.status(401).json({
|
|
||||||
error: "Maximum TOTP attempts reached",
|
|
||||||
code: "TOTP_MAX_ATTEMPTS",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
existingSession.totpAttempts++;
|
|
||||||
if (!responseSent) {
|
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
res.json({
|
|
||||||
requires_totp: true,
|
if (pendingTOTPSessions[sessionId]) {
|
||||||
sessionId,
|
const responses = prompts.map((p) => {
|
||||||
prompt: prompts[totpPromptIndex].prompt,
|
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||||
attempts_remaining: 3 - existingSession.totpAttempts,
|
return resolvedCredentials.password;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseSent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
responseSent = true;
|
|
||||||
keyboardInteractiveResponded = true;
|
keyboardInteractiveResponded = true;
|
||||||
|
|
||||||
pendingTOTPSessions[sessionId] = {
|
pendingTOTPSessions[sessionId] = {
|
||||||
@@ -768,36 +766,35 @@ app.post("/docker/ssh/connect", async (req, res) => {
|
|||||||
resolvedCredentials.authType !== "none";
|
resolvedCredentials.authType !== "none";
|
||||||
|
|
||||||
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
||||||
if (pendingTOTPSessions[sessionId]) {
|
if (responseSent) {
|
||||||
const existingSession = pendingTOTPSessions[sessionId];
|
const responses = prompts.map((p) => {
|
||||||
if (existingSession.totpAttempts >= 3) {
|
if (
|
||||||
if (!responseSent) {
|
/password/i.test(p.prompt) &&
|
||||||
responseSent = true;
|
resolvedCredentials.password
|
||||||
delete pendingTOTPSessions[sessionId];
|
) {
|
||||||
client.end();
|
return resolvedCredentials.password;
|
||||||
res.status(401).json({
|
|
||||||
error: "Maximum password attempts reached",
|
|
||||||
code: "PASSWORD_MAX_ATTEMPTS",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
existingSession.totpAttempts++;
|
|
||||||
if (!responseSent) {
|
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
res.json({
|
|
||||||
requires_totp: true,
|
if (pendingTOTPSessions[sessionId]) {
|
||||||
sessionId,
|
const responses = prompts.map((p) => {
|
||||||
prompt: prompts[passwordPromptIndex].prompt,
|
if (
|
||||||
isPassword: true,
|
/password/i.test(p.prompt) &&
|
||||||
attempts_remaining: 3 - existingSession.totpAttempts,
|
resolvedCredentials.password
|
||||||
});
|
) {
|
||||||
|
return resolvedCredentials.password;
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseSent) return;
|
|
||||||
responseSent = true;
|
|
||||||
keyboardInteractiveResponded = true;
|
keyboardInteractiveResponded = true;
|
||||||
|
|
||||||
pendingTOTPSessions[sessionId] = {
|
pendingTOTPSessions[sessionId] = {
|
||||||
|
|||||||
@@ -390,6 +390,15 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
if (sshSessions[sessionId]?.isConnected) {
|
if (sshSessions[sessionId]?.isConnected) {
|
||||||
cleanupSession(sessionId);
|
cleanupSession(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up any stale pending TOTP sessions
|
||||||
|
if (pendingTOTPSessions[sessionId]) {
|
||||||
|
try {
|
||||||
|
pendingTOTPSessions[sessionId].client.end();
|
||||||
|
} catch {}
|
||||||
|
delete pendingTOTPSessions[sessionId];
|
||||||
|
}
|
||||||
|
|
||||||
const client = new SSHClient();
|
const client = new SSHClient();
|
||||||
|
|
||||||
let resolvedCredentials = { password, sshKey, keyPassword, authType };
|
let resolvedCredentials = { password, sshKey, keyPassword, authType };
|
||||||
@@ -450,9 +459,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
host: ip,
|
host: ip,
|
||||||
port,
|
port,
|
||||||
username,
|
username,
|
||||||
tryKeyboard:
|
tryKeyboard: true,
|
||||||
resolvedCredentials.authType === "none" ||
|
|
||||||
forceKeyboardInteractive === true,
|
|
||||||
keepaliveInterval: 30000,
|
keepaliveInterval: 30000,
|
||||||
keepaliveCountMax: 3,
|
keepaliveCountMax: 3,
|
||||||
readyTimeout: 60000,
|
readyTimeout: 60000,
|
||||||
@@ -555,9 +562,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
.json({ error: "Password required for password authentication" });
|
.json({ error: "Password required for password authentication" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!forceKeyboardInteractive) {
|
|
||||||
config.password = resolvedCredentials.password;
|
config.password = resolvedCredentials.password;
|
||||||
}
|
|
||||||
} else if (resolvedCredentials.authType === "none") {
|
} else if (resolvedCredentials.authType === "none") {
|
||||||
} else {
|
} else {
|
||||||
fileLogger.warn(
|
fileLogger.warn(
|
||||||
@@ -684,37 +689,29 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (totpPromptIndex !== -1) {
|
if (totpPromptIndex !== -1) {
|
||||||
if (pendingTOTPSessions[sessionId]) {
|
if (responseSent) {
|
||||||
const existingSession = pendingTOTPSessions[sessionId];
|
const responses = prompts.map((p) => {
|
||||||
if (existingSession.totpAttempts >= 3) {
|
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||||
if (!responseSent) {
|
return resolvedCredentials.password;
|
||||||
responseSent = true;
|
|
||||||
delete pendingTOTPSessions[sessionId];
|
|
||||||
client.end();
|
|
||||||
res.status(401).json({
|
|
||||||
error: "Maximum TOTP attempts reached",
|
|
||||||
code: "TOTP_MAX_ATTEMPTS",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
existingSession.totpAttempts++;
|
|
||||||
if (!responseSent) {
|
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
res.json({
|
|
||||||
requires_totp: true,
|
if (pendingTOTPSessions[sessionId]) {
|
||||||
sessionId,
|
const responses = prompts.map((p) => {
|
||||||
prompt: prompts[totpPromptIndex].prompt,
|
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||||
attempts_remaining: 3 - existingSession.totpAttempts,
|
return resolvedCredentials.password;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseSent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
responseSent = true;
|
|
||||||
keyboardInteractiveResponded = true;
|
keyboardInteractiveResponded = true;
|
||||||
|
|
||||||
pendingTOTPSessions[sessionId] = {
|
pendingTOTPSessions[sessionId] = {
|
||||||
@@ -765,38 +762,29 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
if (!hasStoredPassword && passwordPromptIndex !== -1) {
|
||||||
if (pendingTOTPSessions[sessionId]) {
|
if (responseSent) {
|
||||||
const existingSession = pendingTOTPSessions[sessionId];
|
const responses = prompts.map((p) => {
|
||||||
if (existingSession.totpAttempts >= 3) {
|
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||||
if (!responseSent) {
|
return resolvedCredentials.password;
|
||||||
responseSent = true;
|
|
||||||
delete pendingTOTPSessions[sessionId];
|
|
||||||
client.end();
|
|
||||||
res.status(401).json({
|
|
||||||
error: "Maximum password attempts reached",
|
|
||||||
code: "PASSWORD_MAX_ATTEMPTS",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
existingSession.totpAttempts++;
|
|
||||||
if (!responseSent) {
|
|
||||||
responseSent = true;
|
responseSent = true;
|
||||||
res.json({
|
|
||||||
requires_totp: true,
|
if (pendingTOTPSessions[sessionId]) {
|
||||||
sessionId,
|
const responses = prompts.map((p) => {
|
||||||
prompt: prompts[passwordPromptIndex].prompt,
|
if (/password/i.test(p.prompt) && resolvedCredentials.password) {
|
||||||
isPassword: true,
|
return resolvedCredentials.password;
|
||||||
attempts_remaining: 3 - existingSession.totpAttempts,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
finish(responses);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseSent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
responseSent = true;
|
|
||||||
keyboardInteractiveResponded = true;
|
keyboardInteractiveResponded = true;
|
||||||
|
|
||||||
pendingTOTPSessions[sessionId] = {
|
pendingTOTPSessions[sessionId] = {
|
||||||
|
|||||||
@@ -456,6 +456,8 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
const totpCode = totpData.code;
|
const totpCode = totpData.code;
|
||||||
totpAttempts++;
|
totpAttempts++;
|
||||||
keyboardInteractiveFinish([totpCode]);
|
keyboardInteractiveFinish([totpCode]);
|
||||||
|
keyboardInteractiveFinish = null;
|
||||||
|
totpPromptSent = false;
|
||||||
} else {
|
} else {
|
||||||
sshLogger.warn("TOTP response received but no callback available", {
|
sshLogger.warn("TOTP response received but no callback available", {
|
||||||
operation: "totp_response_error",
|
operation: "totp_response_error",
|
||||||
@@ -482,6 +484,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
}
|
}
|
||||||
const password = passwordData.code;
|
const password = passwordData.code;
|
||||||
keyboardInteractiveFinish([password]);
|
keyboardInteractiveFinish([password]);
|
||||||
|
keyboardInteractiveFinish = null;
|
||||||
} else {
|
} else {
|
||||||
sshLogger.warn(
|
sshLogger.warn(
|
||||||
"Password response received but no callback available",
|
"Password response received but no callback available",
|
||||||
@@ -988,29 +991,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
|
|
||||||
if (totpPromptIndex !== -1) {
|
if (totpPromptIndex !== -1) {
|
||||||
if (totpPromptSent) {
|
if (totpPromptSent) {
|
||||||
if (totpAttempts >= 3) {
|
sshLogger.warn("TOTP prompt asked again - ignoring duplicate", {
|
||||||
sshLogger.error("TOTP maximum attempts reached", {
|
operation: "ssh_keyboard_interactive_totp_duplicate",
|
||||||
operation: "ssh_keyboard_interactive_totp_max_attempts",
|
|
||||||
hostId: id,
|
hostId: id,
|
||||||
attempts: totpAttempts,
|
prompts: promptTexts,
|
||||||
});
|
});
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "error",
|
|
||||||
message: "Maximum TOTP attempts reached",
|
|
||||||
code: "TOTP_MAX_ATTEMPTS",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
cleanupSSH();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "totp_retry",
|
|
||||||
attempts_remaining: 3 - totpAttempts,
|
|
||||||
prompt: prompts[totpPromptIndex].prompt,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
totpPromptSent = true;
|
totpPromptSent = true;
|
||||||
@@ -1032,19 +1017,22 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
finish(responses);
|
finish(responses);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set timeout for TOTP response
|
||||||
totpTimeout = setTimeout(() => {
|
totpTimeout = setTimeout(() => {
|
||||||
if (keyboardInteractiveFinish) {
|
if (keyboardInteractiveFinish) {
|
||||||
keyboardInteractiveFinish = null;
|
keyboardInteractiveFinish = null;
|
||||||
totpPromptSent = false;
|
totpPromptSent = false;
|
||||||
totpAttempts = 0;
|
sshLogger.warn("TOTP prompt timeout", {
|
||||||
|
operation: "totp_timeout",
|
||||||
|
hostId: id,
|
||||||
|
});
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "error",
|
type: "error",
|
||||||
message: "TOTP verification timeout",
|
message: "TOTP verification timeout. Please reconnect.",
|
||||||
code: "TOTP_TIMEOUT",
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
cleanupSSH();
|
cleanupSSH(connectionTimeout);
|
||||||
}
|
}
|
||||||
}, 180000);
|
}, 180000);
|
||||||
|
|
||||||
@@ -1082,6 +1070,25 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
finish(responses);
|
finish(responses);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Set timeout for password response
|
||||||
|
totpTimeout = setTimeout(() => {
|
||||||
|
if (keyboardInteractiveFinish) {
|
||||||
|
keyboardInteractiveFinish = null;
|
||||||
|
keyboardInteractiveResponded = false;
|
||||||
|
sshLogger.warn("Password prompt timeout", {
|
||||||
|
operation: "password_timeout",
|
||||||
|
hostId: id,
|
||||||
|
});
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "error",
|
||||||
|
message: "Password verification timeout. Please reconnect.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
cleanupSSH(connectionTimeout);
|
||||||
|
}
|
||||||
|
}, 180000);
|
||||||
|
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "password_required",
|
type: "password_required",
|
||||||
@@ -1107,9 +1114,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
host: ip,
|
host: ip,
|
||||||
port,
|
port,
|
||||||
username,
|
username,
|
||||||
tryKeyboard:
|
tryKeyboard: true,
|
||||||
resolvedCredentials.authType === "none" ||
|
|
||||||
hostConfig.forceKeyboardInteractive === true,
|
|
||||||
keepaliveInterval: 30000,
|
keepaliveInterval: 30000,
|
||||||
keepaliveCountMax: 3,
|
keepaliveCountMax: 3,
|
||||||
readyTimeout: 30000,
|
readyTimeout: 30000,
|
||||||
@@ -1191,9 +1196,7 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hostConfig.forceKeyboardInteractive) {
|
|
||||||
connectConfig.password = resolvedCredentials.password;
|
connectConfig.password = resolvedCredentials.password;
|
||||||
}
|
|
||||||
} else if (
|
} else if (
|
||||||
resolvedCredentials.authType === "key" &&
|
resolvedCredentials.authType === "key" &&
|
||||||
resolvedCredentials.key
|
resolvedCredentials.key
|
||||||
@@ -1385,6 +1388,11 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (totpTimeout) {
|
||||||
|
clearTimeout(totpTimeout);
|
||||||
|
totpTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (sshStream) {
|
if (sshStream) {
|
||||||
try {
|
try {
|
||||||
sshStream.end();
|
sshStream.end();
|
||||||
@@ -1409,11 +1417,6 @@ wss.on("connection", async (ws: WebSocket, req) => {
|
|||||||
sshConn = null;
|
sshConn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totpTimeout) {
|
|
||||||
clearTimeout(totpTimeout);
|
|
||||||
totpTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
totpPromptSent = false;
|
totpPromptSent = false;
|
||||||
totpAttempts = 0;
|
totpAttempts = 0;
|
||||||
isKeyboardInteractive = false;
|
isKeyboardInteractive = false;
|
||||||
|
|||||||
2381
src/locales/ar.json
2381
src/locales/ar.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/bn.json
2381
src/locales/bn.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/cs.json
2381
src/locales/cs.json
File diff suppressed because it is too large
Load Diff
2380
src/locales/de.json
2380
src/locales/de.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/el.json
2381
src/locales/el.json
File diff suppressed because it is too large
Load Diff
@@ -218,6 +218,7 @@
|
|||||||
"run": "Run",
|
"run": "Run",
|
||||||
"empty": "No snippets yet",
|
"empty": "No snippets yet",
|
||||||
"emptyHint": "Create a snippet to save commonly used commands",
|
"emptyHint": "Create a snippet to save commonly used commands",
|
||||||
|
"searchSnippets": "Search snippets...",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"content": "Command",
|
"content": "Command",
|
||||||
@@ -283,7 +284,7 @@
|
|||||||
"deleteSuccess": "Command deleted from history",
|
"deleteSuccess": "Command deleted from history",
|
||||||
"deleteFailed": "Failed to delete command.",
|
"deleteFailed": "Failed to delete command.",
|
||||||
"deleteTooltip": "Delete command",
|
"deleteTooltip": "Delete command",
|
||||||
"tabHint": "Use Tab in Terminal to autocomplete from command history",
|
"tabHint": "Use Tab in Terminal to autocomplete from command history if enabled in User Profile",
|
||||||
"authRequiredRefresh": "Authentication required. Please refresh the page.",
|
"authRequiredRefresh": "Authentication required. Please refresh the page.",
|
||||||
"dataAccessLockedReauth": "Data access locked. Please re-authenticate.",
|
"dataAccessLockedReauth": "Data access locked. Please re-authenticate.",
|
||||||
"loading": "Loading command history...",
|
"loading": "Loading command history...",
|
||||||
@@ -1417,7 +1418,7 @@
|
|||||||
"connectToServer": "Connect to a Server",
|
"connectToServer": "Connect to a Server",
|
||||||
"selectServerToEdit": "Select a server from the sidebar to start editing files",
|
"selectServerToEdit": "Select a server from the sidebar to start editing files",
|
||||||
"fileOperations": "File Operations",
|
"fileOperations": "File Operations",
|
||||||
"confirmDeleteMessage": "Are you sure you want to delete <strong>{{name}}</strong>?",
|
"confirmDeleteMessage": "Are you sure you want to delete {{name}}?",
|
||||||
"confirmDeleteSingleItem": "Are you sure you want to permanently delete \"{{name}}\"?",
|
"confirmDeleteSingleItem": "Are you sure you want to permanently delete \"{{name}}\"?",
|
||||||
"confirmDeleteMultipleItems": "Are you sure you want to permanently delete {{count}} items?",
|
"confirmDeleteMultipleItems": "Are you sure you want to permanently delete {{count}} items?",
|
||||||
"confirmDeleteMultipleItemsWithFolders": "Are you sure you want to permanently delete {{count}} items? This includes folders and their contents.",
|
"confirmDeleteMultipleItemsWithFolders": "Are you sure you want to permanently delete {{count}} items? This includes folders and their contents.",
|
||||||
|
|||||||
2381
src/locales/es.json
2381
src/locales/es.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/fr.json
2381
src/locales/fr.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/he.json
2381
src/locales/he.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/hi.json
2381
src/locales/hi.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/id.json
2381
src/locales/id.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/it.json
2381
src/locales/it.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/ja.json
2381
src/locales/ja.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/ko.json
2381
src/locales/ko.json
File diff suppressed because it is too large
Load Diff
2374
src/locales/nb.json
2374
src/locales/nb.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/nl.json
2381
src/locales/nl.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/pl.json
2381
src/locales/pl.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/pt.json
2381
src/locales/pt.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/ro.json
2381
src/locales/ro.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/ru.json
2381
src/locales/ru.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/sv.json
2381
src/locales/sv.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/th.json
2381
src/locales/th.json
File diff suppressed because it is too large
Load Diff
2378
src/locales/tr.json
2378
src/locales/tr.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/uk.json
2381
src/locales/uk.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/vi.json
2381
src/locales/vi.json
File diff suppressed because it is too large
Load Diff
2381
src/locales/zh.json
2381
src/locales/zh.json
File diff suppressed because it is too large
Load Diff
@@ -32,6 +32,7 @@ interface DockerManagerProps {
|
|||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isTopbarOpen?: boolean;
|
isTopbarOpen?: boolean;
|
||||||
embedded?: boolean;
|
embedded?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DockerManager({
|
export function DockerManager({
|
||||||
@@ -40,6 +41,7 @@ export function DockerManager({
|
|||||||
isVisible = true,
|
isVisible = true,
|
||||||
isTopbarOpen = true,
|
isTopbarOpen = true,
|
||||||
embedded = false,
|
embedded = false,
|
||||||
|
onClose,
|
||||||
}: DockerManagerProps): React.ReactElement {
|
}: DockerManagerProps): React.ReactElement {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { state: sidebarState } = useSidebar();
|
const { state: sidebarState } = useSidebar();
|
||||||
@@ -177,6 +179,7 @@ export function DockerManager({
|
|||||||
);
|
);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
setIsValidating(false);
|
setIsValidating(false);
|
||||||
|
onClose?.();
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
@@ -347,6 +350,7 @@ export function DockerManager({
|
|||||||
toast.error(error instanceof Error ? error.message : "Failed to connect");
|
toast.error(error instanceof Error ? error.message : "Failed to connect");
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
setIsValidating(false);
|
setIsValidating(false);
|
||||||
|
onClose?.();
|
||||||
} finally {
|
} finally {
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
}
|
}
|
||||||
@@ -355,6 +359,7 @@ export function DockerManager({
|
|||||||
const handleAuthCancel = () => {
|
const handleAuthCancel = () => {
|
||||||
setShowAuthDialog(false);
|
setShowAuthDialog(false);
|
||||||
setIsConnecting(false);
|
setIsConnecting(false);
|
||||||
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const topMarginPx = isTopbarOpen ? 74 : 16;
|
const topMarginPx = isTopbarOpen ? 74 : 16;
|
||||||
|
|||||||
@@ -675,6 +675,11 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
}
|
}
|
||||||
if (reconnectAttempts.current > 0) {
|
if (reconnectAttempts.current > 0) {
|
||||||
attemptReconnection();
|
attemptReconnection();
|
||||||
|
} else {
|
||||||
|
shouldNotReconnectRef.current = true;
|
||||||
|
if (onClose) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 35000);
|
}, 35000);
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export function SSHToolsSidebar({
|
|||||||
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
|
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
|
||||||
return shouldCollapse ? new Set() : new Set();
|
return shouldCollapse ? new Set() : new Set();
|
||||||
});
|
});
|
||||||
|
const [snippetSearchQuery, setSnippetSearchQuery] = useState("");
|
||||||
const [showFolderDialog, setShowFolderDialog] = useState(false);
|
const [showFolderDialog, setShowFolderDialog] = useState(false);
|
||||||
const [editingFolder, setEditingFolder] = useState<SnippetFolder | null>(
|
const [editingFolder, setEditingFolder] = useState<SnippetFolder | null>(
|
||||||
null,
|
null,
|
||||||
@@ -772,7 +773,22 @@ export function SSHToolsSidebar({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
snippets.forEach((snippet) => {
|
const filteredSnippets = snippetSearchQuery
|
||||||
|
? snippets.filter(
|
||||||
|
(snippet) =>
|
||||||
|
snippet.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(snippetSearchQuery.toLowerCase()) ||
|
||||||
|
snippet.content
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(snippetSearchQuery.toLowerCase()) ||
|
||||||
|
snippet.description
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(snippetSearchQuery.toLowerCase()),
|
||||||
|
)
|
||||||
|
: snippets;
|
||||||
|
|
||||||
|
filteredSnippets.forEach((snippet) => {
|
||||||
const folderName = snippet.folder || "";
|
const folderName = snippet.folder || "";
|
||||||
if (!grouped.has(folderName)) {
|
if (!grouped.has(folderName)) {
|
||||||
grouped.set(folderName, []);
|
grouped.set(folderName, []);
|
||||||
@@ -1280,6 +1296,28 @@ 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("snippets.searchSnippets")}
|
||||||
|
value={snippetSearchQuery}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSnippetSearchQuery(e.target.value);
|
||||||
|
}}
|
||||||
|
className="pl-10 pr-10"
|
||||||
|
/>
|
||||||
|
{snippetSearchQuery && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0"
|
||||||
|
onClick={() => setSnippetSearchQuery("")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
|
|||||||
@@ -368,6 +368,7 @@ export function AppView({
|
|||||||
isVisible={effectiveVisible}
|
isVisible={effectiveVisible}
|
||||||
isTopbarOpen={isTopbarOpen}
|
isTopbarOpen={isTopbarOpen}
|
||||||
embedded
|
embedded
|
||||||
|
onClose={() => removeTab(t.id)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FileManager
|
<FileManager
|
||||||
|
|||||||
Reference in New Issue
Block a user