Add file upload for SSH private key and certificate fields (#15)
* Add editable display-name field to generic integrations Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop, Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate from the host/IP field, mirroring the SSH host rename pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4 * Surface the new-integration name field as a labeled input The name field for new generic integrations was a faint header input with only placeholder text, easy to miss. Move it into the form grid as a proper labeled "Name" field next to the other connection fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4 * Add file upload for SSH private key and certificate fields Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting its contents into the Private Key / OPKSSH Certificate fields. * Fix SSH private key paste corrupting multi-line PEM format Private Key and Certificate fields were single-line <input> elements, which strip newlines on paste and corrupt PEM-formatted keys (causing 'Unsupported key format' errors). Render them as multi-line textareas instead so pasted keys keep their line breaks. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c23724bade
commit
b2b4709abe
1 changed files with 43 additions and 4 deletions
|
|
@ -35,7 +35,7 @@ const accentColors = [
|
|||
{ name: 'Red', color: '#E74C3C' },
|
||||
]
|
||||
|
||||
type FieldDef = { key: string; label: string; secret?: boolean; hint?: string; placeholder?: string }
|
||||
type FieldDef = { key: string; label: string; secret?: boolean; hint?: string; placeholder?: string; file?: boolean }
|
||||
|
||||
const integrationTypeDefs: { type: string; name: string; multiInstance?: boolean; fields: FieldDef[] }[] = [
|
||||
{ type: 'proxmox', name: 'Proxmox', multiInstance: true, fields: [
|
||||
|
|
@ -83,9 +83,9 @@ const sshFields: FieldDef[] = [
|
|||
{ key: 'port', label: 'Port (default 22)' },
|
||||
{ key: 'username', label: 'Username' },
|
||||
{ key: 'password', label: 'Password', secret: true },
|
||||
{ key: 'privateKey', label: 'Private Key (PEM)', secret: true },
|
||||
{ key: 'privateKey', label: 'Private Key (PEM)', secret: true, file: true },
|
||||
{ key: 'passphrase', label: 'Key Passphrase', secret: true },
|
||||
{ key: 'certificate', label: 'OPKSSH Certificate (id_key-cert.pub)', secret: true },
|
||||
{ key: 'certificate', label: 'OPKSSH Certificate (id_key-cert.pub)', secret: true, file: true },
|
||||
]
|
||||
|
||||
const cardBase: React.CSSProperties = {
|
||||
|
|
@ -385,6 +385,7 @@ function SshHostsSection() {
|
|||
const [busy, setBusy] = useState<Set<number>>(new Set())
|
||||
const [newDrafts, setNewDrafts] = useState<{ key: number; values: Record<string, string> }[]>([])
|
||||
const nextNewKey = useRef(-1)
|
||||
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({})
|
||||
|
||||
useEffect(() => {
|
||||
refresh()
|
||||
|
|
@ -546,12 +547,50 @@ function SshHostsSection() {
|
|||
const isRevealed = revealed.has(key)
|
||||
const savedValue = f.secret ? '' : existing?.config[f.key] ?? ''
|
||||
const value = values[f.key] ?? savedValue
|
||||
|
||||
if (f.file) {
|
||||
return (
|
||||
<div key={key} className="col-span-3">
|
||||
<label style={labelStyle}>{f.label}</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
style={{ ...inputStyle, height: '90px', paddingRight: '32px', fontFamily: 'monospace', resize: 'vertical', whiteSpace: 'pre' }}
|
||||
value={value}
|
||||
onChange={(e) => onChange(f.key, e.target.value)}
|
||||
placeholder={existing ? '•••••••••••• (paste to replace)' : 'Paste key contents here, or upload a file'}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
ref={(el) => { fileInputRefs.current[key] = el }}
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => onChange(f.key, String(reader.result ?? '').trim())
|
||||
reader.readAsText(file)
|
||||
e.target.value = ''
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => fileInputRefs.current[key]?.click()}
|
||||
title="Upload from file"
|
||||
className="absolute cursor-pointer border-none bg-transparent"
|
||||
style={{ right: '8px', top: '8px', color: '#7A7D85' }}
|
||||
>
|
||||
<Upload size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
<label style={labelStyle}>{f.label}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
style={inputStyle}
|
||||
style={{ ...inputStyle, paddingRight: f.secret ? '32px' : undefined }}
|
||||
type={f.secret && !isRevealed ? 'password' : 'text'}
|
||||
value={value}
|
||||
onChange={(e) => onChange(f.key, e.target.value)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue