feat: Add Shell button for interactive LXC container access (#144)
* feat: Add Shell button for interactive LXC container access - Add Shell button to ScriptInstallationCard for SSH scripts with container_id - Implement shell state management in InstalledScriptsTab - Add shell execution methods in server.js (local and SSH) - Add isShell prop to Terminal component - Implement smooth scrolling to terminal when opened - Add highlight effect for better UX - Shell sessions are interactive (no auto-commands like update) The Shell button provides direct interactive access to LXC containers without automatically sending update commands, allowing users to manually execute commands in the container shell. * fix: Include SSH authentication fields in installed scripts data - Add SSH key fields (auth_type, ssh_key, ssh_key_passphrase, ssh_port) to database query - Update InstalledScript interface to include SSH authentication fields - Fix server data construction in handleOpenShell and handleUpdateScript - Now properly supports SSH key authentication for shell and update operations This fixes the issue where SSH key authentication was not being used even when configured in server settings, as the installed scripts data was missing the SSH authentication fields. * fix: Resolve TypeScript and ESLint build errors - Replace logical OR (||) with nullish coalescing (??) operators - Remove unnecessary type assertion for container_id - Add missing dependencies to useEffect and useCallback hooks - Remove unused variable in SSHKeyInput component - Add isShell property to WebSocketMessage type definition - Fix ServerInfo type to allow null in shell execution methods All TypeScript and ESLint errors resolved, build now passes successfully.
This commit is contained in:
committed by
GitHub
parent
a5b67b183b
commit
546d7290ee
144
server.js
144
server.js
@@ -51,6 +51,7 @@ const handle = app.getRequestHandler();
|
|||||||
* @property {string} [mode]
|
* @property {string} [mode]
|
||||||
* @property {ServerInfo} [server]
|
* @property {ServerInfo} [server]
|
||||||
* @property {boolean} [isUpdate]
|
* @property {boolean} [isUpdate]
|
||||||
|
* @property {boolean} [isShell]
|
||||||
* @property {string} [containerId]
|
* @property {string} [containerId]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -207,13 +208,15 @@ class ScriptExecutionHandler {
|
|||||||
* @param {WebSocketMessage} message
|
* @param {WebSocketMessage} message
|
||||||
*/
|
*/
|
||||||
async handleMessage(ws, message) {
|
async handleMessage(ws, message) {
|
||||||
const { action, scriptPath, executionId, input, mode, server, isUpdate, containerId } = message;
|
const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, containerId } = message;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'start':
|
case 'start':
|
||||||
if (scriptPath && executionId) {
|
if (scriptPath && executionId) {
|
||||||
if (isUpdate && containerId) {
|
if (isUpdate && containerId) {
|
||||||
await this.startUpdateExecution(ws, containerId, executionId, mode, server);
|
await this.startUpdateExecution(ws, containerId, executionId, mode, server);
|
||||||
|
} else if (isShell && containerId) {
|
||||||
|
await this.startShellExecution(ws, containerId, executionId, mode, server);
|
||||||
} else {
|
} else {
|
||||||
await this.startScriptExecution(ws, scriptPath, executionId, mode, server);
|
await this.startScriptExecution(ws, scriptPath, executionId, mode, server);
|
||||||
}
|
}
|
||||||
@@ -709,6 +712,145 @@ class ScriptExecutionHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start shell execution
|
||||||
|
* @param {ExtendedWebSocket} ws
|
||||||
|
* @param {string} containerId
|
||||||
|
* @param {string} executionId
|
||||||
|
* @param {string} mode
|
||||||
|
* @param {ServerInfo|null} server
|
||||||
|
*/
|
||||||
|
async startShellExecution(ws, containerId, executionId, mode = 'local', server = null) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Send start message
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'start',
|
||||||
|
data: `Starting shell session for container ${containerId}...`,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mode === 'ssh' && server) {
|
||||||
|
await this.startSSHShellExecution(ws, containerId, executionId, server);
|
||||||
|
} else {
|
||||||
|
await this.startLocalShellExecution(ws, containerId, executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'error',
|
||||||
|
data: `Failed to start shell: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start local shell execution
|
||||||
|
* @param {ExtendedWebSocket} ws
|
||||||
|
* @param {string} containerId
|
||||||
|
* @param {string} executionId
|
||||||
|
*/
|
||||||
|
async startLocalShellExecution(ws, containerId, executionId) {
|
||||||
|
const { spawn } = await import('node-pty');
|
||||||
|
|
||||||
|
// Create a shell process that will run pct enter
|
||||||
|
const childProcess = spawn('bash', ['-c', `pct enter ${containerId}`], {
|
||||||
|
name: 'xterm-color',
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: process.env
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the execution
|
||||||
|
this.activeExecutions.set(executionId, {
|
||||||
|
process: childProcess,
|
||||||
|
ws
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle pty data
|
||||||
|
childProcess.onData((data) => {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'output',
|
||||||
|
data: data.toString(),
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: No automatic command is sent - user can type commands interactively
|
||||||
|
|
||||||
|
// Handle process exit
|
||||||
|
childProcess.onExit((e) => {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'end',
|
||||||
|
data: `Shell session ended with exit code: ${e.exitCode}`,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeExecutions.delete(executionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start SSH shell execution
|
||||||
|
* @param {ExtendedWebSocket} ws
|
||||||
|
* @param {string} containerId
|
||||||
|
* @param {string} executionId
|
||||||
|
* @param {ServerInfo} server
|
||||||
|
*/
|
||||||
|
async startSSHShellExecution(ws, containerId, executionId, server) {
|
||||||
|
const sshService = getSSHExecutionService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const execution = await sshService.executeCommand(
|
||||||
|
server,
|
||||||
|
`pct enter ${containerId}`,
|
||||||
|
/** @param {string} data */
|
||||||
|
(data) => {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'output',
|
||||||
|
data: data,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** @param {string} error */
|
||||||
|
(error) => {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'error',
|
||||||
|
data: error,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** @param {number} code */
|
||||||
|
(code) => {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'end',
|
||||||
|
data: `Shell session ended with exit code: ${code}`,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeExecutions.delete(executionId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store the execution
|
||||||
|
this.activeExecutions.set(executionId, {
|
||||||
|
process: /** @type {any} */ (execution).process,
|
||||||
|
ws
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: No automatic command is sent - user can type commands interactively
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
type: 'error',
|
||||||
|
data: `SSH shell execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TerminalHandler removed - not used by current application
|
// TerminalHandler removed - not used by current application
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ interface InstalledScript {
|
|||||||
server_ip: string | null;
|
server_ip: string | null;
|
||||||
server_user: string | null;
|
server_user: string | null;
|
||||||
server_password: string | null;
|
server_password: string | null;
|
||||||
|
server_auth_type: string | null;
|
||||||
|
server_ssh_key: string | null;
|
||||||
|
server_ssh_key_passphrase: string | null;
|
||||||
|
server_ssh_port: number | null;
|
||||||
server_color: string | null;
|
server_color: string | null;
|
||||||
installation_date: string;
|
installation_date: string;
|
||||||
status: 'in_progress' | 'success' | 'failed';
|
status: 'in_progress' | 'success' | 'failed';
|
||||||
@@ -35,6 +39,7 @@ export function InstalledScriptsTab() {
|
|||||||
const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('server_name');
|
const [sortField, setSortField] = useState<'script_name' | 'container_id' | 'server_name' | 'status' | 'installation_date'>('server_name');
|
||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||||
const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null);
|
const [updatingScript, setUpdatingScript] = useState<{ id: number; containerId: string; server?: any } | null>(null);
|
||||||
|
const [openingShell, setOpeningShell] = useState<{ id: number; containerId: string; server?: any } | null>(null);
|
||||||
const [editingScriptId, setEditingScriptId] = useState<number | null>(null);
|
const [editingScriptId, setEditingScriptId] = useState<number | null>(null);
|
||||||
const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' });
|
const [editFormData, setEditFormData] = useState<{ script_name: string; container_id: string }>({ script_name: '', container_id: '' });
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
@@ -340,7 +345,7 @@ export function InstalledScriptsTab() {
|
|||||||
containerStatusMutation.mutate({ serverIds });
|
containerStatusMutation.mutate({ serverIds });
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}, []); // Remove containerStatusMutation from dependencies to prevent loops
|
}, [containerStatusMutation]);
|
||||||
|
|
||||||
// Run cleanup when component mounts and scripts are loaded (only once)
|
// Run cleanup when component mounts and scripts are loaded (only once)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -356,7 +361,7 @@ export function InstalledScriptsTab() {
|
|||||||
console.log('Status check triggered - scripts length:', scripts.length);
|
console.log('Status check triggered - scripts length:', scripts.length);
|
||||||
fetchContainerStatuses();
|
fetchContainerStatuses();
|
||||||
}
|
}
|
||||||
}, [scripts.length]); // Remove fetchContainerStatuses from dependencies
|
}, [scripts.length, fetchContainerStatuses]);
|
||||||
|
|
||||||
// Cleanup timeout on unmount
|
// Cleanup timeout on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -526,13 +531,17 @@ export function InstalledScriptsTab() {
|
|||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
// Get server info if it's SSH mode
|
// Get server info if it's SSH mode
|
||||||
let server = null;
|
let server = null;
|
||||||
if (script.server_id && script.server_user && script.server_password) {
|
if (script.server_id && script.server_user) {
|
||||||
server = {
|
server = {
|
||||||
id: script.server_id,
|
id: script.server_id,
|
||||||
name: script.server_name,
|
name: script.server_name,
|
||||||
ip: script.server_ip,
|
ip: script.server_ip,
|
||||||
user: script.server_user,
|
user: script.server_user,
|
||||||
password: script.server_password
|
password: script.server_password,
|
||||||
|
auth_type: script.server_auth_type ?? 'password',
|
||||||
|
ssh_key: script.server_ssh_key,
|
||||||
|
ssh_key_passphrase: script.server_ssh_key_passphrase,
|
||||||
|
ssh_port: script.server_ssh_port ?? 22
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +559,91 @@ export function InstalledScriptsTab() {
|
|||||||
setUpdatingScript(null);
|
setUpdatingScript(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenShell = (script: InstalledScript) => {
|
||||||
|
if (!script.container_id) {
|
||||||
|
setErrorModal({
|
||||||
|
isOpen: true,
|
||||||
|
title: 'Shell Access Failed',
|
||||||
|
message: 'No Container ID available for this script',
|
||||||
|
details: 'This script does not have a valid container ID and cannot be accessed via shell.'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get server info if it's SSH mode
|
||||||
|
let server = null;
|
||||||
|
if (script.server_id && script.server_user) {
|
||||||
|
server = {
|
||||||
|
id: script.server_id,
|
||||||
|
name: script.server_name,
|
||||||
|
ip: script.server_ip,
|
||||||
|
user: script.server_user,
|
||||||
|
password: script.server_password,
|
||||||
|
auth_type: script.server_auth_type ?? 'password',
|
||||||
|
ssh_key: script.server_ssh_key,
|
||||||
|
ssh_key_passphrase: script.server_ssh_key_passphrase,
|
||||||
|
ssh_port: script.server_ssh_port ?? 22
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpeningShell({
|
||||||
|
id: script.id,
|
||||||
|
containerId: script.container_id,
|
||||||
|
server: server
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseShellTerminal = () => {
|
||||||
|
setOpeningShell(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-scroll to terminals when they open
|
||||||
|
useEffect(() => {
|
||||||
|
if (openingShell) {
|
||||||
|
// Small delay to ensure the terminal is rendered
|
||||||
|
setTimeout(() => {
|
||||||
|
const terminalElement = document.querySelector('[data-terminal="shell"]');
|
||||||
|
if (terminalElement) {
|
||||||
|
// Scroll to the terminal with smooth animation
|
||||||
|
terminalElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
inline: 'nearest'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a subtle highlight effect
|
||||||
|
terminalElement.classList.add('animate-pulse');
|
||||||
|
setTimeout(() => {
|
||||||
|
terminalElement.classList.remove('animate-pulse');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}, [openingShell]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updatingScript) {
|
||||||
|
// Small delay to ensure the terminal is rendered
|
||||||
|
setTimeout(() => {
|
||||||
|
const terminalElement = document.querySelector('[data-terminal="update"]');
|
||||||
|
if (terminalElement) {
|
||||||
|
// Scroll to the terminal with smooth animation
|
||||||
|
terminalElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
inline: 'nearest'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a subtle highlight effect
|
||||||
|
terminalElement.classList.add('animate-pulse');
|
||||||
|
setTimeout(() => {
|
||||||
|
terminalElement.classList.remove('animate-pulse');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}, [updatingScript]);
|
||||||
|
|
||||||
const handleEditScript = (script: InstalledScript) => {
|
const handleEditScript = (script: InstalledScript) => {
|
||||||
setEditingScriptId(script.id);
|
setEditingScriptId(script.id);
|
||||||
setEditFormData({
|
setEditFormData({
|
||||||
@@ -662,7 +756,7 @@ export function InstalledScriptsTab() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Update Terminal */}
|
{/* Update Terminal */}
|
||||||
{updatingScript && (
|
{updatingScript && (
|
||||||
<div className="mb-8">
|
<div className="mb-8" data-terminal="update">
|
||||||
<Terminal
|
<Terminal
|
||||||
scriptPath={`update-${updatingScript.containerId}`}
|
scriptPath={`update-${updatingScript.containerId}`}
|
||||||
onClose={handleCloseUpdateTerminal}
|
onClose={handleCloseUpdateTerminal}
|
||||||
@@ -674,6 +768,20 @@ export function InstalledScriptsTab() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Shell Terminal */}
|
||||||
|
{openingShell && (
|
||||||
|
<div className="mb-8" data-terminal="shell">
|
||||||
|
<Terminal
|
||||||
|
scriptPath={`shell-${openingShell.containerId}`}
|
||||||
|
onClose={handleCloseShellTerminal}
|
||||||
|
mode={openingShell.server ? 'ssh' : 'local'}
|
||||||
|
server={openingShell.server}
|
||||||
|
isShell={true}
|
||||||
|
containerId={openingShell.containerId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Header with Stats */}
|
{/* Header with Stats */}
|
||||||
<div className="bg-card rounded-lg shadow p-6">
|
<div className="bg-card rounded-lg shadow p-6">
|
||||||
<h2 className="text-2xl font-bold text-foreground mb-4">Installed Scripts</h2>
|
<h2 className="text-2xl font-bold text-foreground mb-4">Installed Scripts</h2>
|
||||||
@@ -995,6 +1103,7 @@ export function InstalledScriptsTab() {
|
|||||||
onSave={handleSaveEdit}
|
onSave={handleSaveEdit}
|
||||||
onCancel={handleCancelEdit}
|
onCancel={handleCancelEdit}
|
||||||
onUpdate={() => handleUpdateScript(script)}
|
onUpdate={() => handleUpdateScript(script)}
|
||||||
|
onShell={() => handleOpenShell(script)}
|
||||||
onDelete={() => handleDeleteScript(Number(script.id))}
|
onDelete={() => handleDeleteScript(Number(script.id))}
|
||||||
isUpdating={updateScriptMutation.isPending}
|
isUpdating={updateScriptMutation.isPending}
|
||||||
isDeleting={deleteScriptMutation.isPending}
|
isDeleting={deleteScriptMutation.isPending}
|
||||||
@@ -1203,6 +1312,17 @@ export function InstalledScriptsTab() {
|
|||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{/* Shell button - only show for SSH scripts with container_id */}
|
||||||
|
{script.container_id && script.execution_mode === 'ssh' && (
|
||||||
|
<Button
|
||||||
|
onClick={() => handleOpenShell(script)}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={containerStatuses.get(script.id) === 'stopped'}
|
||||||
|
>
|
||||||
|
Shell
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
||||||
{script.container_id && script.execution_mode === 'ssh' && (
|
{script.container_id && script.execution_mode === 'ssh' && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -104,9 +104,6 @@ export function SSHKeyInput({ value, onChange, onError, disabled = false }: SSHK
|
|||||||
keyType = 'ECDSA';
|
keyType = 'ECDSA';
|
||||||
} else if (keyLine.includes('OPENSSH PRIVATE KEY')) {
|
} else if (keyLine.includes('OPENSSH PRIVATE KEY')) {
|
||||||
// For OpenSSH format keys, try to detect type from the key content
|
// For OpenSSH format keys, try to detect type from the key content
|
||||||
// Look for common patterns in the base64 content
|
|
||||||
const base64Content = keyContent.replace(/-----BEGIN.*?-----/, '').replace(/-----END.*?-----/, '').replace(/\s/g, '');
|
|
||||||
|
|
||||||
// This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns
|
// This is a heuristic - OpenSSH ED25519 keys typically start with specific patterns
|
||||||
// We'll default to "OpenSSH" for now since we can't reliably detect the type
|
// We'll default to "OpenSSH" for now since we can't reliably detect the type
|
||||||
keyType = 'OpenSSH';
|
keyType = 'OpenSSH';
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ interface InstalledScript {
|
|||||||
server_ip: string | null;
|
server_ip: string | null;
|
||||||
server_user: string | null;
|
server_user: string | null;
|
||||||
server_password: string | null;
|
server_password: string | null;
|
||||||
|
server_auth_type: string | null;
|
||||||
|
server_ssh_key: string | null;
|
||||||
|
server_ssh_key_passphrase: string | null;
|
||||||
|
server_ssh_port: number | null;
|
||||||
server_color: string | null;
|
server_color: string | null;
|
||||||
installation_date: string;
|
installation_date: string;
|
||||||
status: 'in_progress' | 'success' | 'failed';
|
status: 'in_progress' | 'success' | 'failed';
|
||||||
@@ -31,6 +35,7 @@ interface ScriptInstallationCardProps {
|
|||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
|
onShell: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
isUpdating: boolean;
|
isUpdating: boolean;
|
||||||
isDeleting: boolean;
|
isDeleting: boolean;
|
||||||
@@ -50,6 +55,7 @@ export function ScriptInstallationCard({
|
|||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
|
onShell,
|
||||||
onDelete,
|
onDelete,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
@@ -203,6 +209,18 @@ export function ScriptInstallationCard({
|
|||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{/* Shell button - only show for SSH scripts with container_id */}
|
||||||
|
{script.container_id && script.execution_mode === 'ssh' && (
|
||||||
|
<Button
|
||||||
|
onClick={onShell}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 min-w-0"
|
||||||
|
disabled={containerStatus === 'stopped'}
|
||||||
|
>
|
||||||
|
Shell
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
{/* Container Control Buttons - only show for SSH scripts with container_id */}
|
||||||
{script.container_id && script.execution_mode === 'ssh' && (
|
{script.container_id && script.execution_mode === 'ssh' && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface TerminalProps {
|
|||||||
mode?: 'local' | 'ssh';
|
mode?: 'local' | 'ssh';
|
||||||
server?: any;
|
server?: any;
|
||||||
isUpdate?: boolean;
|
isUpdate?: boolean;
|
||||||
|
isShell?: boolean;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ interface TerminalMessage {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, containerId }: TerminalProps) {
|
export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate = false, isShell = false, containerId }: TerminalProps) {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [isClient, setIsClient] = useState(false);
|
||||||
@@ -332,6 +333,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
|||||||
mode,
|
mode,
|
||||||
server,
|
server,
|
||||||
isUpdate,
|
isUpdate,
|
||||||
|
isShell,
|
||||||
containerId
|
containerId
|
||||||
};
|
};
|
||||||
ws.send(JSON.stringify(message));
|
ws.send(JSON.stringify(message));
|
||||||
@@ -372,7 +374,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
|||||||
wsRef.current.close();
|
wsRef.current.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [scriptPath, mode, server, isUpdate, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const startScript = () => {
|
const startScript = () => {
|
||||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) {
|
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) {
|
||||||
@@ -388,6 +390,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
|||||||
mode,
|
mode,
|
||||||
server,
|
server,
|
||||||
isUpdate,
|
isUpdate,
|
||||||
|
isShell,
|
||||||
containerId
|
containerId
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,10 @@ class DatabaseService {
|
|||||||
s.ip as server_ip,
|
s.ip as server_ip,
|
||||||
s.user as server_user,
|
s.user as server_user,
|
||||||
s.password as server_password,
|
s.password as server_password,
|
||||||
|
s.auth_type as server_auth_type,
|
||||||
|
s.ssh_key as server_ssh_key,
|
||||||
|
s.ssh_key_passphrase as server_ssh_key_passphrase,
|
||||||
|
s.ssh_port as server_ssh_port,
|
||||||
s.color as server_color
|
s.color as server_color
|
||||||
FROM installed_scripts inst
|
FROM installed_scripts inst
|
||||||
LEFT JOIN servers s ON inst.server_id = s.id
|
LEFT JOIN servers s ON inst.server_id = s.id
|
||||||
|
|||||||
Reference in New Issue
Block a user