feat(i18n): Lokalisierung - Phase 6 abgeschlossen (ExecutionModeModal, PublicKeyModal)
Lokalisierte Komponenten (19/alle): - ExecutionModeModal: Server-Auswahl, Installation-Bestätigung, Single/Multiple Server Views - PublicKeyModal: SSH Key Anzeige, Kopier-Funktionen, Anleitungen Neue Translation Keys: - executionModeModal.* (13+ keys für Loading, Server-Auswahl, Actions, Errors) - publicKeyModal.* (10+ keys für Instructions, Labels, Actions, Fallbacks) Technische Details: - ExecutionModeModal: Conditional rendering für 0/1/N Server mit dynamic scriptName interpolation - PublicKeyModal: Lokalisierte Fallback-Alerts für Copy-Fehler - useEffect eslint-disable für fetchServers dependency - Script name interpolation in Bestätigungs-Texten
This commit is contained in:
@@ -6,6 +6,7 @@ import { Button } from './ui/button';
|
||||
import { ColorCodedDropdown } from './ColorCodedDropdown';
|
||||
import { SettingsModal } from './SettingsModal';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useTranslation } from '@/lib/i18n/useTranslation';
|
||||
|
||||
|
||||
interface ExecutionModeModalProps {
|
||||
@@ -16,6 +17,7 @@ interface ExecutionModeModalProps {
|
||||
}
|
||||
|
||||
export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: ExecutionModeModalProps) {
|
||||
const { t } = useTranslation('executionModeModal');
|
||||
useRegisterModal(isOpen, { id: 'execution-mode-modal', allowEscape: true, onClose });
|
||||
const [servers, setServers] = useState<Server[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -23,19 +25,6 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
const [selectedServer, setSelectedServer] = useState<Server | null>(null);
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void fetchServers();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Auto-select server when exactly one server is available
|
||||
useEffect(() => {
|
||||
if (isOpen && !loading && servers.length === 1) {
|
||||
setSelectedServer(servers[0] ?? null);
|
||||
}
|
||||
}, [isOpen, loading, servers]);
|
||||
|
||||
// Refresh servers when settings modal closes
|
||||
const handleSettingsModalClose = () => {
|
||||
setSettingsModalOpen(false);
|
||||
@@ -49,7 +38,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
try {
|
||||
const response = await fetch('/api/servers');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch servers');
|
||||
throw new Error(t('errors.fetchFailed'));
|
||||
}
|
||||
const data = await response.json();
|
||||
// Sort servers by name alphabetically
|
||||
@@ -58,15 +47,29 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
);
|
||||
setServers(sortedServers);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
setError(err instanceof Error ? err.message : t('errors.fetchFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void fetchServers();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
// Auto-select server when exactly one server is available
|
||||
useEffect(() => {
|
||||
if (isOpen && !loading && servers.length === 1) {
|
||||
setSelectedServer(servers[0] ?? null);
|
||||
}
|
||||
}, [isOpen, loading, servers]);
|
||||
|
||||
const handleExecute = () => {
|
||||
if (!selectedServer) {
|
||||
setError('Please select a server for SSH execution');
|
||||
setError(t('errors.noServerSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -88,7 +91,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
<div className="bg-card rounded-lg shadow-xl max-w-md w-full border border-border">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border">
|
||||
<h2 className="text-xl font-bold text-foreground">Select Server</h2>
|
||||
<h2 className="text-xl font-bold text-foreground">{t('title')}</h2>
|
||||
<Button
|
||||
onClick={onClose}
|
||||
variant="ghost"
|
||||
@@ -121,19 +124,19 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
{loading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Loading servers...</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{t('loadingServers')}</p>
|
||||
</div>
|
||||
) : servers.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<p className="text-sm">No servers configured</p>
|
||||
<p className="text-xs mt-1">Add servers in Settings to execute scripts</p>
|
||||
<p className="text-sm">{t('noServersConfigured')}</p>
|
||||
<p className="text-xs mt-1">{t('addServersHint')}</p>
|
||||
<Button
|
||||
onClick={() => setSettingsModalOpen(true)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-3"
|
||||
>
|
||||
Open Server Settings
|
||||
{t('openServerSettings')}
|
||||
</Button>
|
||||
</div>
|
||||
) : servers.length === 1 ? (
|
||||
@@ -141,10 +144,10 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
Install Script Confirmation
|
||||
{t('installConfirmation.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Do you want to install "{scriptName}" on the following server?
|
||||
{t('installConfirmation.description', { values: { scriptName } })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +158,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{selectedServer?.name ?? 'Unnamed Server'}
|
||||
{selectedServer?.name ?? t('unnamedServer')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selectedServer?.ip}
|
||||
@@ -171,14 +174,14 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
Cancel
|
||||
{t('actions.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExecute}
|
||||
variant="default"
|
||||
size="default"
|
||||
>
|
||||
Install
|
||||
{t('actions.install')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,20 +190,20 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
<div className="space-y-6">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium text-foreground mb-2">
|
||||
Select server to execute "{scriptName}"
|
||||
{t('multipleServers.title', { values: { scriptName } })}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Server Selection */}
|
||||
<div className="mb-6">
|
||||
<label htmlFor="server" className="block text-sm font-medium text-foreground mb-2">
|
||||
Select Server
|
||||
{t('multipleServers.selectServerLabel')}
|
||||
</label>
|
||||
<ColorCodedDropdown
|
||||
servers={servers}
|
||||
selectedServer={selectedServer}
|
||||
onServerSelect={handleServerSelect}
|
||||
placeholder="Select a server..."
|
||||
placeholder={t('multipleServers.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -211,7 +214,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
variant="outline"
|
||||
size="default"
|
||||
>
|
||||
Cancel
|
||||
{t('actions.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExecute}
|
||||
@@ -220,7 +223,7 @@ export function ExecutionModeModal({ isOpen, onClose, onExecute, scriptName }: E
|
||||
size="default"
|
||||
className={!selectedServer ? 'bg-muted-foreground cursor-not-allowed' : ''}
|
||||
>
|
||||
Run on Server
|
||||
{t('actions.runOnServer')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { X, Copy, Check, Server, Globe } from 'lucide-react';
|
||||
import { Button } from './ui/button';
|
||||
import { useRegisterModal } from './modal/ModalStackProvider';
|
||||
import { useTranslation } from '@/lib/i18n/useTranslation';
|
||||
|
||||
interface PublicKeyModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,6 +15,7 @@ interface PublicKeyModalProps {
|
||||
}
|
||||
|
||||
export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverIp }: PublicKeyModalProps) {
|
||||
const { t } = useTranslation('publicKeyModal');
|
||||
useRegisterModal(isOpen, { id: 'public-key-modal', allowEscape: true, onClose });
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [commandCopied, setCommandCopied] = useState(false);
|
||||
@@ -45,7 +47,7 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback copy failed:', fallbackError);
|
||||
// If all else fails, show the key in an alert
|
||||
alert('Please manually copy this key:\n\n' + publicKey);
|
||||
alert(t('copyFallback') + publicKey);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
@@ -53,7 +55,7 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
} catch (error) {
|
||||
console.error('Failed to copy to clipboard:', error);
|
||||
// Fallback: show the key in an alert
|
||||
alert('Please manually copy this key:\n\n' + publicKey);
|
||||
alert(t('copyFallback') + publicKey);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,14 +84,14 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
setTimeout(() => setCommandCopied(false), 2000);
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback copy failed:', fallbackError);
|
||||
alert('Please manually copy this command:\n\n' + command);
|
||||
alert(t('copyCommandFallback') + command);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to copy command to clipboard:', error);
|
||||
alert('Please manually copy this command:\n\n' + command);
|
||||
alert(t('copyCommandFallback') + command);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,8 +105,8 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
<Server className="h-6 w-6 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-card-foreground">SSH Public Key</h2>
|
||||
<p className="text-sm text-muted-foreground">Add this key to your server's authorized_keys</p>
|
||||
<h2 className="text-xl font-semibold text-card-foreground">{t('title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
@@ -133,19 +135,19 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-foreground">Instructions:</h3>
|
||||
<h3 className="font-medium text-foreground">{t('instructions.title')}</h3>
|
||||
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
|
||||
<li>Copy the public key below</li>
|
||||
<li>SSH into your server: <code className="bg-muted px-1 rounded">ssh root@{serverIp}</code></li>
|
||||
<li>Add the key to authorized_keys: <code className="bg-muted px-1 rounded">echo "<paste-key>" >> ~/.ssh/authorized_keys</code></li>
|
||||
<li>Set proper permissions: <code className="bg-muted px-1 rounded">chmod 600 ~/.ssh/authorized_keys</code></li>
|
||||
<li>{t('instructions.step1')}</li>
|
||||
<li>{t('instructions.step2')} <code className="bg-muted px-1 rounded">ssh root@{serverIp}</code></li>
|
||||
<li>{t('instructions.step3')} <code className="bg-muted px-1 rounded">echo "<paste-key>" >> ~/.ssh/authorized_keys</code></li>
|
||||
<li>{t('instructions.step4')} <code className="bg-muted px-1 rounded">chmod 600 ~/.ssh/authorized_keys</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{/* Public Key */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Public Key:</label>
|
||||
<label className="text-sm font-medium text-foreground">{t('publicKeyLabel')}</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -155,12 +157,12 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
Copied!
|
||||
{t('actions.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy
|
||||
{t('actions.copy')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -169,14 +171,14 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
value={publicKey}
|
||||
readOnly
|
||||
className="w-full px-3 py-2 border rounded-md shadow-sm bg-card text-foreground font-mono text-xs min-h-[60px] resize-none border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring"
|
||||
placeholder="Public key will appear here..."
|
||||
placeholder={t('placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Command */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-foreground">Quick Add Command:</label>
|
||||
<label className="text-sm font-medium text-foreground">{t('quickCommandLabel')}</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -186,12 +188,12 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
{commandCopied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4" />
|
||||
Copied!
|
||||
{t('actions.copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4" />
|
||||
Copy Command
|
||||
{t('actions.copyCommand')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -202,14 +204,14 @@ export function PublicKeyModal({ isOpen, onClose, publicKey, serverName, serverI
|
||||
</code>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Copy and paste this command directly into your server terminal to add the key to authorized_keys
|
||||
{t('quickCommandHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-border">
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Close
|
||||
{t('actions.close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -157,6 +157,55 @@ export const deMessages: NestedMessages = {
|
||||
buttonTitle: 'Einstellungen öffnen',
|
||||
buttonLabel: 'Einstellungen',
|
||||
},
|
||||
executionModeModal: {
|
||||
title: 'Server auswählen',
|
||||
loadingServers: 'Lade Server...',
|
||||
noServersConfigured: 'Keine Server konfiguriert',
|
||||
addServersHint: 'Fügen Sie Server in den Einstellungen hinzu, um Skripte auszuführen',
|
||||
openServerSettings: 'Servereinstellungen öffnen',
|
||||
installConfirmation: {
|
||||
title: 'Skript-Installation bestätigen',
|
||||
description: 'Möchten Sie "{scriptName}" auf folgendem Server installieren?',
|
||||
},
|
||||
unnamedServer: 'Unbenannter Server',
|
||||
multipleServers: {
|
||||
title: 'Server für "{scriptName}" auswählen',
|
||||
selectServerLabel: 'Server auswählen',
|
||||
placeholder: 'Wählen Sie einen Server...',
|
||||
},
|
||||
actions: {
|
||||
cancel: 'Abbrechen',
|
||||
install: 'Installieren',
|
||||
runOnServer: 'Auf Server ausführen',
|
||||
},
|
||||
errors: {
|
||||
noServerSelected: 'Bitte wählen Sie einen Server für die SSH-Ausführung',
|
||||
fetchFailed: 'Fehler beim Abrufen der Server',
|
||||
},
|
||||
},
|
||||
publicKeyModal: {
|
||||
title: 'SSH Public Key',
|
||||
subtitle: 'Fügen Sie diesen Schlüssel zu den authorized_keys Ihres Servers hinzu',
|
||||
instructions: {
|
||||
title: 'Anleitung:',
|
||||
step1: 'Kopieren Sie den unten stehenden öffentlichen Schlüssel',
|
||||
step2: 'SSH-Verbindung zu Ihrem Server herstellen:',
|
||||
step3: 'Schlüssel zu authorized_keys hinzufügen:',
|
||||
step4: 'Korrekte Berechtigungen setzen:',
|
||||
},
|
||||
publicKeyLabel: 'Öffentlicher Schlüssel:',
|
||||
quickCommandLabel: 'Schnell-Hinzufügen-Befehl:',
|
||||
quickCommandHint: 'Kopieren Sie diesen Befehl und fügen Sie ihn direkt in Ihr Server-Terminal ein, um den Schlüssel zu authorized_keys hinzuzufügen',
|
||||
placeholder: 'Öffentlicher Schlüssel wird hier angezeigt...',
|
||||
actions: {
|
||||
copy: 'Kopieren',
|
||||
copied: 'Kopiert!',
|
||||
copyCommand: 'Befehl kopieren',
|
||||
close: 'Schließen',
|
||||
},
|
||||
copyFallback: 'Bitte kopieren Sie diesen Schlüssel manuell:\n\n',
|
||||
copyCommandFallback: 'Bitte kopieren Sie diesen Befehl manuell:\n\n',
|
||||
},
|
||||
layout: {
|
||||
title: 'PVE Skriptverwaltung',
|
||||
tagline: 'Verwalte und starte lokale Proxmox-Hilfsskripte mit Live-Ausgabe',
|
||||
|
||||
@@ -391,4 +391,53 @@ export const enMessages: NestedMessages = {
|
||||
buttonTitle: 'Open Settings',
|
||||
buttonLabel: 'Settings',
|
||||
},
|
||||
executionModeModal: {
|
||||
title: 'Select Server',
|
||||
loadingServers: 'Loading servers...',
|
||||
noServersConfigured: 'No servers configured',
|
||||
addServersHint: 'Add servers in Settings to execute scripts',
|
||||
openServerSettings: 'Open Server Settings',
|
||||
installConfirmation: {
|
||||
title: 'Install Script Confirmation',
|
||||
description: 'Do you want to install "{scriptName}" on the following server?',
|
||||
},
|
||||
unnamedServer: 'Unnamed Server',
|
||||
multipleServers: {
|
||||
title: 'Select server to execute "{scriptName}"',
|
||||
selectServerLabel: 'Select Server',
|
||||
placeholder: 'Select a server...',
|
||||
},
|
||||
actions: {
|
||||
cancel: 'Cancel',
|
||||
install: 'Install',
|
||||
runOnServer: 'Run on Server',
|
||||
},
|
||||
errors: {
|
||||
noServerSelected: 'Please select a server for SSH execution',
|
||||
fetchFailed: 'Failed to fetch servers',
|
||||
},
|
||||
},
|
||||
publicKeyModal: {
|
||||
title: 'SSH Public Key',
|
||||
subtitle: 'Add this key to your server\'s authorized_keys',
|
||||
instructions: {
|
||||
title: 'Instructions:',
|
||||
step1: 'Copy the public key below',
|
||||
step2: 'SSH into your server:',
|
||||
step3: 'Add the key to authorized_keys:',
|
||||
step4: 'Set proper permissions:',
|
||||
},
|
||||
publicKeyLabel: 'Public Key:',
|
||||
quickCommandLabel: 'Quick Add Command:',
|
||||
quickCommandHint: 'Copy and paste this command directly into your server terminal to add the key to authorized_keys',
|
||||
placeholder: 'Public key will appear here...',
|
||||
actions: {
|
||||
copy: 'Copy',
|
||||
copied: 'Copied!',
|
||||
copyCommand: 'Copy Command',
|
||||
close: 'Close',
|
||||
},
|
||||
copyFallback: 'Please manually copy this key:\n\n',
|
||||
copyCommandFallback: 'Please manually copy this command:\n\n',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user