fix: credential picker in host modal, fix xterm dimensions crash
- Added credential dropdown to New Host modal (loads from vault API) - Fixed xterm.js "Cannot read dimensions" crash by guarding fitAddon.fit() with requestAnimationFrame and container dimension checks - Added WebGL context loss handler - credentialId now passed when creating hosts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7e347ce378
commit
1f32ce4620
@ -33,13 +33,23 @@ export function useTerminal() {
|
|||||||
term.open(container)
|
term.open(container)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
term.loadAddon(new WebglAddon())
|
const webgl = new WebglAddon()
|
||||||
|
webgl.onContextLoss(() => { webgl.dispose() })
|
||||||
|
term.loadAddon(webgl)
|
||||||
} catch {
|
} catch {
|
||||||
// WebGL not available, fall back to canvas
|
// WebGL not available, fall back to canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delay fit until container has layout dimensions
|
||||||
|
const safeFit = () => {
|
||||||
|
try {
|
||||||
|
if (container.offsetWidth > 0 && container.offsetHeight > 0) {
|
||||||
fitAddon.fit()
|
fitAddon.fit()
|
||||||
const resizeObserver = new ResizeObserver(() => fitAddon.fit())
|
}
|
||||||
|
} catch { /* ignore fit errors on unmounted containers */ }
|
||||||
|
}
|
||||||
|
requestAnimationFrame(safeFit)
|
||||||
|
const resizeObserver = new ResizeObserver(() => safeFit())
|
||||||
resizeObserver.observe(container)
|
resizeObserver.observe(container)
|
||||||
|
|
||||||
return { term, fitAddon, searchAddon, resizeObserver }
|
return { term, fitAddon, searchAddon, resizeObserver }
|
||||||
|
|||||||
@ -17,6 +17,10 @@ const selectedHost = ref<any>(null)
|
|||||||
const hostCredential = ref<any>(null)
|
const hostCredential = ref<any>(null)
|
||||||
const loadingCredential = ref(false)
|
const loadingCredential = ref(false)
|
||||||
|
|
||||||
|
// Credentials list for host modal dropdown
|
||||||
|
const allCredentials = ref<any[]>([])
|
||||||
|
const allSshKeys = ref<any[]>([])
|
||||||
|
|
||||||
// Terminal composable for connect-on-click
|
// Terminal composable for connect-on-click
|
||||||
const { createTerminal, connectToHost } = useTerminal()
|
const { createTerminal, connectToHost } = useTerminal()
|
||||||
|
|
||||||
@ -24,9 +28,18 @@ onMounted(async () => {
|
|||||||
await Promise.all([connections.fetchHosts(), connections.fetchTree()])
|
await Promise.all([connections.fetchHosts(), connections.fetchTree()])
|
||||||
})
|
})
|
||||||
|
|
||||||
function openNewHost(groupId?: number) {
|
async function openNewHost(groupId?: number) {
|
||||||
editingHost.value = groupId ? { groupId } : null
|
editingHost.value = groupId ? { groupId } : null
|
||||||
inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: groupId || null, tags: '' }
|
inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: groupId || null, tags: '', credentialId: null }
|
||||||
|
// Load credentials for dropdown
|
||||||
|
try {
|
||||||
|
const [creds, keys] = await Promise.all([
|
||||||
|
vault.listCredentials() as Promise<any[]>,
|
||||||
|
vault.listKeys() as Promise<any[]>,
|
||||||
|
])
|
||||||
|
allCredentials.value = creds
|
||||||
|
allSshKeys.value = keys
|
||||||
|
} catch { /* ignore */ }
|
||||||
showHostDialog.value = true
|
showHostDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,7 +126,7 @@ function dismissSavePrompt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inline modal state
|
// Inline modal state
|
||||||
const inlineHost = ref({ name: '', hostname: '', port: 22, protocol: 'ssh' as 'ssh' | 'rdp', groupId: null as number | null, tags: '' })
|
const inlineHost = ref({ name: '', hostname: '', port: 22, protocol: 'ssh' as 'ssh' | 'rdp', groupId: null as number | null, tags: '', credentialId: null as number | null })
|
||||||
|
|
||||||
async function createGroupInline() {
|
async function createGroupInline() {
|
||||||
const nameEl = document.getElementById('grp-name') as HTMLInputElement
|
const nameEl = document.getElementById('grp-name') as HTMLInputElement
|
||||||
@ -134,10 +147,11 @@ async function createHostInline() {
|
|||||||
port: inlineHost.value.port,
|
port: inlineHost.value.port,
|
||||||
protocol: inlineHost.value.protocol,
|
protocol: inlineHost.value.protocol,
|
||||||
groupId: inlineHost.value.groupId,
|
groupId: inlineHost.value.groupId,
|
||||||
|
credentialId: inlineHost.value.credentialId,
|
||||||
tags,
|
tags,
|
||||||
})
|
})
|
||||||
showHostDialog.value = false
|
showHostDialog.value = false
|
||||||
inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: null, tags: '' }
|
inlineHost.value = { name: '', hostname: '', port: 22, protocol: 'ssh', groupId: null, tags: '', credentialId: null }
|
||||||
await connections.fetchTree()
|
await connections.fetchTree()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,6 +433,15 @@ async function deleteSelectedHost() {
|
|||||||
<option value="rdp">RDP</option>
|
<option value="rdp">RDP</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-gray-400 mb-1">Credential</label>
|
||||||
|
<select v-model="inlineHost.credentialId" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded text-white">
|
||||||
|
<option :value="null">— None —</option>
|
||||||
|
<option v-for="cred in allCredentials" :key="cred.id" :value="cred.id">
|
||||||
|
{{ cred.name }} ({{ cred.username || cred.type }})
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm text-gray-400 mb-1">Tags <span class="text-gray-600">(comma separated)</span></label>
|
<label class="block text-sm text-gray-400 mb-1">Tags <span class="text-gray-600">(comma separated)</span></label>
|
||||||
<input v-model="inlineHost.tags" type="text" placeholder="ssh, prod, web"
|
<input v-model="inlineHost.tags" type="text" placeholder="ssh, prod, web"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user