chore: update translations
This commit is contained in:
@@ -9,12 +9,13 @@ import ptbrTranslation from "../locales/pt-BR/translation.json";
|
||||
import ruTranslation from "../locales/ru/translation.json";
|
||||
import frTranslation from "../locales/fr/translation.json";
|
||||
import koTranslation from "../locales/ko/translation.json";
|
||||
import itTranslation from "../locales/it/translation.json";
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
supportedLngs: ["en", "zh", "de", "ptbr", "ru", "fr", "ko"],
|
||||
supportedLngs: ["en", "zh", "de", "ptbr", "ru", "fr", "ko", "it"],
|
||||
fallbackLng: "en",
|
||||
debug: false,
|
||||
|
||||
@@ -48,6 +49,9 @@ i18n
|
||||
ko: {
|
||||
translation: koTranslation,
|
||||
},
|
||||
it: {
|
||||
translation: itTranslation,
|
||||
},
|
||||
},
|
||||
|
||||
interpolation: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -160,12 +160,30 @@
|
||||
"generateEd25519": "Generate Ed25519",
|
||||
"generateECDSA": "Generate ECDSA",
|
||||
"generateRSA": "Generate RSA",
|
||||
"keyTypeEcdsaP256": "ECDSA P-256 (SSH)",
|
||||
"keyTypeEcdsaP384": "ECDSA P-384 (SSH)",
|
||||
"keyTypeEcdsaP521": "ECDSA P-521 (SSH)",
|
||||
"keyTypeDsa": "DSA (SSH)",
|
||||
"keyTypeRsaSha256": "RSA-SHA2-256",
|
||||
"keyTypeRsaSha512": "RSA-SHA2-512"
|
||||
},
|
||||
"keyPairGeneratedSuccessfully": "{{keyType}} key pair generated successfully",
|
||||
"failedToGenerateKeyPair": "Failed to generate key pair",
|
||||
"generateKeyPairNote": "Generate a new SSH key pair directly. This will replace any existing keys in the form.",
|
||||
"invalidKey": "Invalid Key",
|
||||
"detectionError": "Detection Error",
|
||||
"unknown": "Unknown"
|
||||
"removing": "Removing:",
|
||||
"clickToEditCredential": "Click to edit credential",
|
||||
"dragToMoveBetweenFolders": "Drag to move between folders",
|
||||
"keyBasedOnlyForDeployment": "Only SSH key-based credentials can be deployed",
|
||||
"publicKeyRequiredForDeployment": "Public key is required for deployment",
|
||||
"selectTargetHost": "Please select a target host",
|
||||
"keyDeployedSuccessfully": "SSH key deployed successfully",
|
||||
"deploymentFailed": "Deployment failed",
|
||||
"failedToDeployKey": "Failed to deploy SSH key",
|
||||
"clickToRenameFolder": "Click to rename folder",
|
||||
"renameFolder": "Rename folder",
|
||||
"idLabel": "ID:"
|
||||
},
|
||||
"dragIndicator": {
|
||||
"error": "Error: {{error}}",
|
||||
@@ -188,7 +206,10 @@
|
||||
"commandsWillBeSent": "Commands will be sent to {{count}} selected terminal(s).",
|
||||
"settings": "Settings",
|
||||
"enableRightClickCopyPaste": "Enable right‑click copy/paste",
|
||||
"shareIdeas": "Have ideas for what should come next for ssh tools? Share them on"
|
||||
"shareIdeas": "Have ideas for what should come next for ssh tools? Share them on",
|
||||
"scripts": {
|
||||
"inputPlaceholder": "e.g., System Commands, Docker Scripts"
|
||||
}
|
||||
},
|
||||
"snippets": {
|
||||
"title": "Snippets",
|
||||
@@ -222,7 +243,34 @@
|
||||
"runTooltip": "Execute this snippet in the terminal",
|
||||
"copyTooltip": "Copy snippet to clipboard",
|
||||
"editTooltip": "Edit this snippet",
|
||||
"deleteTooltip": "Delete this snippet"
|
||||
"deleteTooltip": "Delete this snippet",
|
||||
"newFolder": "New Folder",
|
||||
"reorderSameFolder": "Can only reorder snippets within the same folder",
|
||||
"reorderSuccess": "Snippets reordered successfully",
|
||||
"reorderFailed": "Failed to reorder snippets",
|
||||
"deleteFolderConfirm": "Delete folder \"{{name}}\"? All snippets will be moved to Uncategorized.",
|
||||
"deleteFolderSuccess": "Folder deleted successfully",
|
||||
"deleteFolderFailed": "Failed to delete folder",
|
||||
"updateFolderSuccess": "Folder updated successfully",
|
||||
"createFolderSuccess": "Folder created successfully",
|
||||
"updateFolderFailed": "Failed to update folder",
|
||||
"createFolderFailed": "Failed to create folder",
|
||||
"selectTerminals": "Select Terminals (optional)",
|
||||
"executeOnSelected": "Execute on {{count}} selected terminal(s)",
|
||||
"executeOnCurrent": "Execute on current terminal (click to select multiple)",
|
||||
"folder": "Folder",
|
||||
"selectFolder": "Select a folder or leave empty",
|
||||
"noFolder": "No folder (Uncategorized)",
|
||||
"folderName": "Folder Name",
|
||||
"folderNameRequired": "Folder name is required",
|
||||
"folderColor": "Folder Color",
|
||||
"folderIcon": "Folder Icon",
|
||||
"preview": "Preview",
|
||||
"updateFolder": "Update Folder",
|
||||
"createFolder": "Create Folder",
|
||||
"editFolder": "Edit Folder",
|
||||
"editFolderDescription": "Customize your snippet folder",
|
||||
"createFolderDescription": "Organize your snippets into folders"
|
||||
},
|
||||
"commandHistory": {
|
||||
"title": "History",
|
||||
@@ -236,7 +284,9 @@
|
||||
"deleteSuccess": "Command deleted from history",
|
||||
"deleteFailed": "Failed to 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",
|
||||
"authRequiredRefresh": "Authentication required. Please refresh the page.",
|
||||
"dataAccessLockedReauth": "Data access locked. Please re-authenticate."
|
||||
},
|
||||
"homepage": {
|
||||
"loggedInTitle": "Logged in!",
|
||||
@@ -309,11 +359,13 @@
|
||||
"home": "Home",
|
||||
"expired": "Expired",
|
||||
"expiresToday": "Expires today",
|
||||
"expiresTomorrow": "Expires tomorrow",
|
||||
"expiresInDays": "Expires in {{days}} days",
|
||||
"expiresTomorrow": "Expires in {{days}} days",
|
||||
"updateAvailable": "Update Available",
|
||||
"sshPath": "SSH Path",
|
||||
"localPath": "Local Path",
|
||||
"appName": "Termix",
|
||||
"resetSidebarWidth": "Reset sidebar width",
|
||||
"dragToResizeSidebar": "Drag to resize sidebar",
|
||||
"noAuthCredentials": "No authentication credentials available for this SSH host",
|
||||
"noReleases": "No Releases",
|
||||
"updatesAndReleases": "Updates & Releases",
|
||||
@@ -412,7 +464,8 @@
|
||||
"sshManager": "SSH Manager",
|
||||
"hostManager": "Host Manager",
|
||||
"cannotSplitTab": "Cannot split this tab",
|
||||
"tabNavigation": "Tab Navigation"
|
||||
"tabNavigation": "Tab Navigation",
|
||||
"hostTabTitle": "{{username}}@{{ip}}:{{port}}"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Admin Settings",
|
||||
@@ -670,7 +723,33 @@
|
||||
"passwordLoginDisabledWarning": "Password login is disabled. Ensure OIDC is properly configured or you will not be able to log in to Termix.",
|
||||
"oidcRequiredWarning": "CRITICAL: Password login is disabled. If you reset or misconfigure OIDC, you will lose all access to Termix and brick your instance. Only proceed if you are absolutely certain.",
|
||||
"confirmDisableOIDCWarning": "WARNING: You are about to disable OIDC while password login is also disabled. This will brick your Termix instance and you will lose all access. Are you absolutely sure you want to proceed?",
|
||||
"failedToUpdatePasswordLoginStatus": "Failed to update password login status"
|
||||
"failedToUpdatePasswordLoginStatus": "Failed to update password login status",
|
||||
"sessionManagement": "Session Management",
|
||||
"loadingSessions": "Loading sessions...",
|
||||
"noActiveSessions": "No active sessions found.",
|
||||
"device": "Device",
|
||||
"user": "User",
|
||||
"created": "Created",
|
||||
"lastActive": "Last Active",
|
||||
"expires": "Expires",
|
||||
"revoked": "Revoked",
|
||||
"revokeAllUserSessionsTitle": "Revoke all sessions for this user",
|
||||
"revokeAll": "Revoke All",
|
||||
"linkOidcToPasswordAccount": "Link OIDC Account to Password Account",
|
||||
"linkOidcToPasswordAccountDescription": "Link {{username}} (OIDC user) to an existing password account. This will enable dual authentication for the password account.",
|
||||
"linkOidcWarningTitle": "Warning: OIDC User Data Will Be Deleted",
|
||||
"linkOidcWarningDescription": "This action will:",
|
||||
"linkOidcActionDeleteUser": "Delete the OIDC user account and all their data",
|
||||
"linkOidcActionAddCapability": "Add OIDC login capability to the target password account",
|
||||
"linkOidcActionDualAuth": "Allow the password account to login with both password and OIDC",
|
||||
"linkTargetUsernameLabel": "Target Password Account Username",
|
||||
"linkTargetUsernamePlaceholder": "Enter username of password account",
|
||||
"linkingAccounts": "Linking...",
|
||||
"linkAccountsButton": "Link Accounts",
|
||||
"passwordMinLength": "Password must be at least 6 characters",
|
||||
"currentRoles": "Current Roles",
|
||||
"noRolesAssigned": "No roles assigned",
|
||||
"assignNewRole": "Assign New Role"
|
||||
},
|
||||
"hosts": {
|
||||
"title": "Host Manager",
|
||||
@@ -731,6 +810,8 @@
|
||||
"enableTunnelDesc": "Enable/disable host visibility in Tunnel tab",
|
||||
"enableFileManager": "Enable File Manager",
|
||||
"enableFileManagerDesc": "Enable/disable host visibility in File Manager tab",
|
||||
"enableDockerDesc": "Enable/disable host visibility in Docker tab",
|
||||
"enableDocker": "Enable Docker",
|
||||
"defaultPath": "Default Path",
|
||||
"defaultPathDesc": "Default directory when opening file manager for this host",
|
||||
"tunnelConnections": "Tunnel Connections",
|
||||
@@ -833,8 +914,25 @@
|
||||
"failedToDeleteHostsInFolder": "Failed to delete hosts in folder",
|
||||
"movedToFolder": "Host \"{{name}}\" moved to \"{{folder}}\" successfully",
|
||||
"failedToMoveToFolder": "Failed to move host to folder",
|
||||
"clickToRenameFolder": "Click to rename folder",
|
||||
"renameFolder": "Rename folder",
|
||||
"removeFromFolder": "Remove from folder \"{{folder}}\"",
|
||||
"editHostTooltip": "Edit host",
|
||||
"deleteHostTooltip": "Delete host",
|
||||
"exportHostTooltip": "Export host",
|
||||
"cloneHostTooltip": "Clone host",
|
||||
"clickToEditHost": "Click to edit host",
|
||||
"dragToMoveBetweenFolders": "Drag to move between folders",
|
||||
"exportedHostConfig": "Exported host configuration for {{name}}",
|
||||
"openTerminal": "Open Terminal",
|
||||
"openFileManager": "Open File Manager",
|
||||
"openTunnels": "Open Tunnels",
|
||||
"openServerDetails": "Open Server Details",
|
||||
"statistics": "Statistics",
|
||||
"enabledWidgets": "Enabled Widgets",
|
||||
"openServerStats": "Open Server Stats",
|
||||
"openFileManager": "Open File Manager",
|
||||
"openTunnels": "Open Tunnels",
|
||||
"enabledWidgetsDesc": "Select which statistics widgets to display for this host",
|
||||
"monitoringConfiguration": "Monitoring Configuration",
|
||||
"monitoringConfigurationDesc": "Configure how often server statistics and status are checked",
|
||||
@@ -985,7 +1083,132 @@
|
||||
"sudoPasswordAutoFill": "Sudo Password Auto-Fill",
|
||||
"sudoPasswordAutoFillDesc": "Automatically offer to insert SSH password when sudo prompts for password",
|
||||
"sudoPassword": "Sudo Password",
|
||||
"sudoPasswordDesc": "Optional password for sudo commands (useful with key authentication)"
|
||||
"sudoPasswordDesc": "Optional password for sudo commands (useful with key authentication)",
|
||||
"socks4": "SOCKS4",
|
||||
"socks5": "SOCKS5",
|
||||
"executeSnippetOnConnect": "Execute a snippet when the terminal connects",
|
||||
"autoMosh": "Auto-MOSH",
|
||||
"autoMoshDesc": "Automatically run MOSH command on connect",
|
||||
"moshCommand": "MOSH Command",
|
||||
"moshCommandDesc": "The MOSH command to execute",
|
||||
"environmentVariables": "Environment Variables",
|
||||
"environmentVariablesDesc": "Set custom environment variables for the terminal session",
|
||||
"variableName": "Variable name",
|
||||
"variableValue": "Value",
|
||||
"addVariable": "Add Variable",
|
||||
"docker": "Docker",
|
||||
"openDocker": "Open Docker",
|
||||
"notEnabled": "Docker is not enabled for this host. Enable it in Host Settings to use Docker features.",
|
||||
"validating": "Validating Docker...",
|
||||
"error": "Error",
|
||||
"errorCode": "Error code: {{code}}",
|
||||
"version": "Docker v{{version}}",
|
||||
"current": "Current",
|
||||
"used_limit": "Used / Limit",
|
||||
"percentage": "Percentage",
|
||||
"input": "Input",
|
||||
"output": "Output",
|
||||
"read": "Read",
|
||||
"write": "Write",
|
||||
"pids": "PIDs",
|
||||
"name": "Name",
|
||||
"id": "ID",
|
||||
"state": "State",
|
||||
"console": "Console",
|
||||
"containerMustBeRunning": "Container must be running to connect to console",
|
||||
"authenticationRequired": "Authentication required",
|
||||
"connectedTo": "Connected to {{containerName}}",
|
||||
"disconnected": "Disconnected",
|
||||
"consoleError": "Console error",
|
||||
"errorMessage": "Error: {{message}}",
|
||||
"failedToConnect": "Failed to connect to console",
|
||||
"disconnectedFromContainer": "Disconnected from container console.",
|
||||
"containerNotRunning": "Container is not running",
|
||||
"startContainerToAccess": "Start the container to access the console",
|
||||
"selectShell": "Select shell",
|
||||
"bash": "Bash",
|
||||
"sh": "Sh",
|
||||
"ash": "Ash",
|
||||
"connecting": "Connecting...",
|
||||
"connect": "Connect",
|
||||
"disconnect": "Disconnect",
|
||||
"notConnected": "Not connected",
|
||||
"clickToConnect": "Click Connect to start an interactive shell",
|
||||
"connectingTo": "Connecting to {{containerName}}...",
|
||||
"containerMustBeRunningToViewStats": "Container must be running to view stats",
|
||||
"failedToFetchStats": "Failed to fetch stats",
|
||||
"noContainersFound": "No containers found",
|
||||
"noContainersFoundHint": "Start by creating containers on your server",
|
||||
"searchPlaceholder": "Search by name, image, or ID...",
|
||||
"filterByStatusPlaceholder": "Filter by status",
|
||||
"allContainersCount": "All ({{count}})",
|
||||
"statusCount": "{{status}} ({{count}})",
|
||||
"noContainersMatchFilters": "No containers match your filters",
|
||||
"noContainersMatchFiltersHint": "Try adjusting your search or filter",
|
||||
"containerStarted": "Container {{name}} started",
|
||||
"failedToStartContainer": "Failed to start container: {{error}}",
|
||||
"containerStopped": "Container {{name}} stopped",
|
||||
"failedToStopContainer": "Failed to stop container: {{error}}",
|
||||
"containerRestarted": "Container {{name}} restarted",
|
||||
"failedToRestartContainer": "Failed to restart container: {{error}}",
|
||||
"containerUnpaused": "Container {{name}} unpaused",
|
||||
"containerPaused": "Container {{name}} paused",
|
||||
"failedToTogglePauseContainer": "Failed to {{action}} container: {{error}}",
|
||||
"containerRemoved": "Container {{name}} removed",
|
||||
"failedToRemoveContainer": "Failed to remove container: {{error}}",
|
||||
"image": "Image:",
|
||||
"idLabel": "ID:",
|
||||
"ports": "Ports:",
|
||||
"noPorts": "None",
|
||||
"created": "Created:",
|
||||
"start": "Start",
|
||||
"stop": "Stop",
|
||||
"unpause": "Unpause",
|
||||
"pause": "Pause",
|
||||
"restart": "Restart",
|
||||
"remove": "Remove",
|
||||
"removeContainer": "Remove Container",
|
||||
"confirmRemoveContainer": "Are you sure you want to remove container \"{{name}}\"?",
|
||||
"runningContainerWarning": "Warning: This container is currently running and will be force-removed.",
|
||||
"removing": "Removing:",
|
||||
"containerNotFound": "Container not found",
|
||||
"backToList": "Back to list",
|
||||
"logs": "Logs",
|
||||
"stats": "Stats",
|
||||
"consoleTab": "Console",
|
||||
"failedToFetchLogs": "Failed to fetch logs: {{error}}",
|
||||
"failedToDownloadLogs": "Failed to download logs: {{error}}",
|
||||
"linesToShow": "Lines to show",
|
||||
"last50Lines": "Last 50 lines",
|
||||
"last100Lines": "Last 100 lines",
|
||||
"last500Lines": "Last 500 lines",
|
||||
"last1000Lines": "Last 1000 lines",
|
||||
"allLogs": "All logs",
|
||||
"showTimestamps": "Show Timestamps",
|
||||
"autoRefresh": "Auto Refresh",
|
||||
"filterLogsPlaceholder": "Filter logs...",
|
||||
"noLogsAvailable": "No logs available"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Split Screen",
|
||||
"none": "None",
|
||||
"twoSplit": "2-Split",
|
||||
"threeSplit": "3-Split",
|
||||
"fourSplit": "4-Split",
|
||||
"availableTabs": "Available Tabs",
|
||||
"dragTabsHint": "Drag tabs into the grid below to position them",
|
||||
"layout": "Layout",
|
||||
"dropHere": "Drop tab here",
|
||||
"apply": "Apply Split",
|
||||
"clear": "Clear",
|
||||
"selectMode": "Select a split mode to get started",
|
||||
"helpText": "Choose how many tabs you want to display at once",
|
||||
"error": {
|
||||
"noAssignments": "Please drag tabs to cells before applying",
|
||||
"fillAllSlots": "Please fill all {{count}} layout spots before applying"
|
||||
},
|
||||
"success": "Split screen applied",
|
||||
"cleared": "Split screen cleared"
|
||||
},
|
||||
"terminal": {
|
||||
"title": "Terminal",
|
||||
@@ -1316,6 +1539,23 @@
|
||||
"loadFileFailed": "Failed to load file: {{error}}",
|
||||
"connectedSuccessfully": "Connected successfully",
|
||||
"totpVerificationFailed": "TOTP verification failed",
|
||||
"sshConnectionFailed": "SSH connection failed. Please check your connection to {{name}} ({{ip}}:{{port}})",
|
||||
"verificationCodePrompt": "Verification code:",
|
||||
"newFolderDefault": "NewFolder",
|
||||
"newFileDefault": "NewFile.txt",
|
||||
"successfullyMovedItems": "Successfully moved {{count}} items to {{target}}",
|
||||
"move": "Move",
|
||||
"searchInFile": "Search in file (Ctrl+F)",
|
||||
"showKeyboardShortcuts": "Show keyboard shortcuts",
|
||||
"startWritingMarkdown": "Start writing your markdown content...",
|
||||
"loadingFileComparison": "Loading file comparison...",
|
||||
"reload": "Reload",
|
||||
"compare": "Compare",
|
||||
"sideBySide": "Side by Side",
|
||||
"inline": "Inline",
|
||||
"fileComparison": "File Comparison: {{file1}} vs {{file2}}",
|
||||
"fileTooLarge": "File too large: {{error}}",
|
||||
"connectedSuccessfully": "Connected successfully",
|
||||
"changePermissions": "Change Permissions",
|
||||
"changePermissionsDesc": "Modify file permissions for",
|
||||
"currentPermissions": "Current Permissions",
|
||||
@@ -1338,7 +1578,7 @@
|
||||
"connecting": "Connecting...",
|
||||
"disconnecting": "Disconnecting...",
|
||||
"unknownTunnelStatus": "Unknown",
|
||||
"unknown": "Unknown",
|
||||
"statusUnknown": "Unknown"
|
||||
"error": "Error",
|
||||
"failed": "Failed",
|
||||
"retrying": "Retrying",
|
||||
@@ -1353,6 +1593,7 @@
|
||||
"attempt": "Attempt {{current}} of {{max}}",
|
||||
"nextRetryIn": "Next retry in {{seconds}} seconds",
|
||||
"checkDockerLogs": "Check your Docker logs for the error reason, join the",
|
||||
"orCreate": "or create a ",,
|
||||
"noTunnelConnections": "No tunnel connections configured",
|
||||
"tunnelConnections": "Tunnel Connections",
|
||||
"addTunnel": "Add Tunnel",
|
||||
@@ -1641,6 +1882,7 @@
|
||||
"commandAutocompleteDesc": "Enable Tab key autocomplete suggestions for terminal commands based on your command history",
|
||||
"defaultSnippetFoldersCollapsed": "Collapse Snippet Folders by Default",
|
||||
"defaultSnippetFoldersCollapsedDesc": "When enabled, all snippet folders will be collapsed when you open the snippets tab",
|
||||
"terminalSyntaxHighlighting": "Terminal Syntax Highlighting",
|
||||
"showHostTags": "Show Host Tags",
|
||||
"showHostTagsDesc": "Display tags under each host in the sidebar. Disable to hide all tags.",
|
||||
"account": "Account",
|
||||
@@ -1704,7 +1946,12 @@
|
||||
"socks5Username": "proxy username",
|
||||
"socks5Password": "proxy password",
|
||||
"socks5PresetName": "e.g., Work VPN Chain",
|
||||
"socks5PresetDescription": "e.g., Proxy chain for accessing work servers"
|
||||
"socks5PresetDescription": "e.g., Proxy chain for accessing work servers",
|
||||
"moshCommand": "mosh user@server",
|
||||
"defaultPort": "22",
|
||||
"defaultEndpointPort": "224",
|
||||
"defaultMaxRetries": "3",
|
||||
"defaultRetryInterval": "10"
|
||||
},
|
||||
"leftSidebar": {
|
||||
"failedToLoadHosts": "Failed to load hosts",
|
||||
@@ -1951,6 +2198,8 @@
|
||||
"noCustomRolesToAssign": "No custom roles available. System roles are auto-assigned.",
|
||||
"credentialSharingWarning": "Credential Authentication Not Supported for Sharing",
|
||||
"credentialSharingWarningDescription": "This host uses credential-based authentication. Shared users will not be able to connect because credentials are encrypted per-user and cannot be shared. Please use password or key-based authentication for hosts you intend to share.",
|
||||
"credentialRequired": "Credential is required when using credential authentication",
|
||||
"credentialRequiredDescription": "This host uses credential-based authentication. Shared users will not be able to connect because credentials are encrypted per-user and cannot be shared. Please use password or key-based authentication for hosts you intend to share.",
|
||||
"auditLogs": "Audit Logs",
|
||||
"viewAuditLogs": "View Audit Logs",
|
||||
"action": "Action",
|
||||
@@ -2060,6 +2309,7 @@
|
||||
"press": "Press",
|
||||
"toToggle": "to toggle",
|
||||
"close": "Close",
|
||||
"hostManager": "Host Manager"
|
||||
"hostManager": "Host Manager",
|
||||
"pressToToggle": "Press Left Shift twice to open the command palette"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1189,7 +1189,7 @@ export function AdminSettings({
|
||||
<TabsContent value="sessions" className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Session Management</h3>
|
||||
<h3 className="text-lg font-semibold">{t("admin.sessionManagement")}</h3>
|
||||
<Button
|
||||
onClick={fetchSessions}
|
||||
disabled={sessionsLoading}
|
||||
@@ -1201,21 +1201,21 @@ export function AdminSettings({
|
||||
</div>
|
||||
{sessionsLoading ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
Loading sessions...
|
||||
{t("admin.loadingSessions")}
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No active sessions found.
|
||||
{t("admin.noActiveSessions")}
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Device</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead>Last Active</TableHead>
|
||||
<TableHead>Expires</TableHead>
|
||||
<TableHead>{t("admin.device")}</TableHead>
|
||||
<TableHead>{t("admin.user")}</TableHead>
|
||||
<TableHead>{t("admin.created")}</TableHead>
|
||||
<TableHead>{t("admin.lastActive")}</TableHead>
|
||||
<TableHead>{t("admin.expires")}</TableHead>
|
||||
<TableHead>{t("admin.actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -1256,7 +1256,7 @@ export function AdminSettings({
|
||||
</span>
|
||||
{session.isRevoked && (
|
||||
<span className="text-xs text-red-600">
|
||||
Revoked
|
||||
{t("admin.revoked")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1297,9 +1297,9 @@ export function AdminSettings({
|
||||
)
|
||||
}
|
||||
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50 text-xs"
|
||||
title="Revoke all sessions for this user"
|
||||
title={t("admin.revokeAllUserSessionsTitle")}
|
||||
>
|
||||
Revoke All
|
||||
{t("admin.revokeAll")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1459,34 +1459,25 @@ export function AdminSettings({
|
||||
>
|
||||
<DialogContent className="sm:max-w-[500px] bg-dark-bg border-2 border-dark-border">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Link2 className="w-5 h-5" />
|
||||
Link OIDC Account to Password Account
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-muted-foreground">
|
||||
Link{" "}
|
||||
<span className="font-mono text-foreground">
|
||||
{linkOidcUser?.username}
|
||||
</span>{" "}
|
||||
(OIDC user) to an existing password account. This will enable
|
||||
dual authentication for the password account.
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Link2 className="w-5 h-5" />
|
||||
{t("admin.linkOidcToPasswordAccount")}
|
||||
</DialogTitle> <DialogDescription className="text-muted-foreground">
|
||||
{t("admin.linkOidcToPasswordAccountDescription", {
|
||||
username: linkOidcUser?.username,
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Warning: OIDC User Data Will Be Deleted</AlertTitle>
|
||||
<AlertTitle>{t("admin.linkOidcWarningTitle")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
This action will:
|
||||
{t("admin.linkOidcWarningDescription")}
|
||||
<ul className="list-disc list-inside mt-2 space-y-1">
|
||||
<li>Delete the OIDC user account and all their data</li>
|
||||
<li>
|
||||
Add OIDC login capability to the target password account
|
||||
</li>
|
||||
<li>
|
||||
Allow the password account to login with both password and
|
||||
OIDC
|
||||
</li>
|
||||
<li>{t("admin.linkOidcActionDeleteUser")}</li>
|
||||
<li>{t("admin.linkOidcActionAddCapability")}</li>
|
||||
<li>{t("admin.linkOidcActionDualAuth")}</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -1496,13 +1487,13 @@ export function AdminSettings({
|
||||
htmlFor="link-target-username"
|
||||
className="text-base font-semibold text-foreground"
|
||||
>
|
||||
Target Password Account Username
|
||||
{t("admin.linkTargetUsernameLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
id="link-target-username"
|
||||
value={linkTargetUsername}
|
||||
onChange={(e) => setLinkTargetUsername(e.target.value)}
|
||||
placeholder="Enter username of password account"
|
||||
placeholder={t("admin.linkTargetUsernamePlaceholder")}
|
||||
disabled={linkLoading}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && linkTargetUsername.trim()) {
|
||||
@@ -1526,7 +1517,9 @@ export function AdminSettings({
|
||||
disabled={linkLoading || !linkTargetUsername.trim()}
|
||||
variant="destructive"
|
||||
>
|
||||
{linkLoading ? "Linking..." : "Link Accounts"}
|
||||
{linkLoading
|
||||
? t("admin.linkingAccounts")
|
||||
: t("admin.linkAccountsButton")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -140,7 +140,7 @@ export function CreateUserDialog({
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Password must be at least 6 characters
|
||||
{t("admin.passwordMinLength")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -901,7 +901,7 @@ export function Auth({
|
||||
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||
}}
|
||||
>
|
||||
TERMIX
|
||||
{t("common.appName").toUpperCase()}
|
||||
</div>
|
||||
<div className="text-lg text-muted-foreground tracking-widest font-light">
|
||||
{t("auth.tagline")}
|
||||
@@ -1389,7 +1389,7 @@ export function Auth({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
Server
|
||||
{t("serverConfig.serverUrl")}
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground truncate max-w-[200px]">
|
||||
{currentServerUrl}
|
||||
@@ -1402,7 +1402,7 @@ export function Auth({
|
||||
onClick={() => setShowServerConfig(true)}
|
||||
className="h-8 px-3"
|
||||
>
|
||||
Edit
|
||||
{t("common.edit")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -120,7 +120,7 @@ export function LeftSidebar({
|
||||
setCurrentTab(sshManagerTab.id);
|
||||
return;
|
||||
}
|
||||
const id = addTab({ type: "ssh_manager", title: "Host Manager" });
|
||||
const id = addTab({ type: "ssh_manager", title: t('nav.hostManager') });
|
||||
setCurrentTab(id);
|
||||
};
|
||||
const adminTab = tabList.find((t) => t.type === "admin");
|
||||
@@ -481,13 +481,13 @@ export function LeftSidebar({
|
||||
<Sidebar variant="floating">
|
||||
<SidebarHeader>
|
||||
<SidebarGroupLabel className="text-lg font-bold text-white">
|
||||
Termix
|
||||
{t('common.appName')}
|
||||
<div className="absolute right-5 flex gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setSidebarWidth(250)}
|
||||
className="w-[28px] h-[28px]"
|
||||
title="Reset sidebar width"
|
||||
title={t("common.resetSidebarWidth")}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -635,7 +635,7 @@ export function LeftSidebar({
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}
|
||||
}}
|
||||
title="Drag to resize sidebar"
|
||||
title={t("common.dragToResizeSidebar")}
|
||||
/>
|
||||
)}
|
||||
</Sidebar>
|
||||
|
||||
@@ -21,10 +21,12 @@ import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext";
|
||||
import { getServerStatusById } from "@/ui/main-axios";
|
||||
import type { HostProps } from "../../../../types";
|
||||
import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
const { addTab } = useTabs();
|
||||
const [host, setHost] = useState(initialHost);
|
||||
const { t } = useTranslation();
|
||||
const [serverStatus, setServerStatus] = useState<
|
||||
"online" | "offline" | "degraded"
|
||||
>("degraded");
|
||||
@@ -177,7 +179,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<Server className="h-4 w-4" />
|
||||
<span className="flex-1">Open Server Stats</span>
|
||||
<span className="flex-1">{t('hosts.openServerStats')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableFileManager && (
|
||||
@@ -188,7 +190,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4" />
|
||||
<span className="flex-1">Open File Manager</span>
|
||||
<span className="flex-1">{t('hosts.openFileManager')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableTunnel && (
|
||||
@@ -199,7 +201,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<ArrowDownUp className="h-4 w-4" />
|
||||
<span className="flex-1">Open Tunnels</span>
|
||||
<span className="flex-1">{t('hosts.openTunnels')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{host.enableDocker && (
|
||||
@@ -210,14 +212,14 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<Container className="h-4 w-4" />
|
||||
<span className="flex-1">Open Docker</span>
|
||||
<span className="flex-1">{t('hosts.openDocker')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
addTab({
|
||||
type: "ssh_manager",
|
||||
title: "Host Manager",
|
||||
title: t('nav.hostManager'),
|
||||
hostConfig: host,
|
||||
initialTab: "add_host",
|
||||
})
|
||||
@@ -225,7 +227,7 @@ export function Host({ host: initialHost }: HostProps): React.ReactElement {
|
||||
className="flex items-center gap-2 cursor-pointer px-3 py-2 hover:bg-dark-hover text-gray-300"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<span className="flex-1">Edit</span>
|
||||
<span className="flex-1">{t('common.edit')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -266,7 +266,11 @@ export function TabProvider({ children }: TabProviderProps) {
|
||||
hostConfig: newHostConfig,
|
||||
title: newHostConfig.name?.trim()
|
||||
? newHostConfig.name
|
||||
: `${newHostConfig.username}@${newHostConfig.ip}:${newHostConfig.port}`,
|
||||
: t("nav.hostTabTitle", {
|
||||
username: newHostConfig.username,
|
||||
ip: newHostConfig.ip,
|
||||
port: newHostConfig.port,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return tab;
|
||||
|
||||
@@ -498,7 +498,7 @@ export function UserProfile({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-gray-300">
|
||||
Terminal Syntax Highlighting{" "}
|
||||
{t("profile.terminalSyntaxHighlighting")}{" "}
|
||||
<span className="text-xs text-yellow-500 font-semibold">
|
||||
(BETA)
|
||||
</span>
|
||||
|
||||
@@ -55,7 +55,7 @@ const AppContent: FC = () => {
|
||||
|
||||
const errorCode = err?.response?.data?.code;
|
||||
if (errorCode === "SESSION_EXPIRED") {
|
||||
console.warn("Session expired - please log in again");
|
||||
console.warn(t("errors.sessionExpired"));
|
||||
}
|
||||
})
|
||||
.finally(() => setAuthLoading(false));
|
||||
|
||||
Reference in New Issue
Block a user