chore: 保存工作副本——客户端授权新契约(双端点/验签/令牌/周检)与桌面会话可靠性改进
This commit is contained in:
1 parent
f71eef0eba
commit
ae6093c9b3
163 files changed
+4911
-2494
No files matched your search
@@ -22,6 +22,7 @@ import DesktopResolutionPicker from '../desktop/DesktopResolutionPicker'
|
||||
import { getDisplayResolution } from '../../utils/desktopResolution'
|
||||
import { useToast } from '../../hooks/useToast'
|
||||
import { translateErrorMessage } from '../../i18n/format'
|
||||
import AppOverlay from '../common/AppOverlay'
|
||||
|
||||
const logger = createLogger('SessionDialog')
|
||||
|
||||
@@ -45,6 +46,18 @@ type KeyInputMode = 'file' | 'text'
|
||||
|
||||
const DEFAULT_KEY_PATH = '~/.ssh/id_ed25519'
|
||||
|
||||
function defaultDisplayName(protocol: SessionProtocol, host: string, username: string): string {
|
||||
if (!host) return ''
|
||||
return protocol === 'vnc' ? host : `${username}@${host}`
|
||||
}
|
||||
|
||||
function isGeneratedDisplayName(name: string, host: string, previousHost = ''): boolean {
|
||||
const trimmed = name.trim()
|
||||
if (!trimmed) return true
|
||||
if (trimmed.includes('@')) return true
|
||||
return [host, previousHost].filter(Boolean).some((value) => trimmed === value || trimmed === `vnc:${value}`)
|
||||
}
|
||||
|
||||
function isPemPrivateKey(value: string | undefined | null): boolean {
|
||||
return Boolean(value && value.includes('-----BEGIN'))
|
||||
}
|
||||
@@ -220,10 +233,9 @@ export default function SessionDialog({
|
||||
host: parsed.host,
|
||||
port: targetIncludesPort(trimmed) ? parsed.port : prev.port,
|
||||
username: parsed.username || prev.username,
|
||||
name:
|
||||
prev.name.trim() && !prev.name.includes('@')
|
||||
? prev.name
|
||||
: `${parsed.username || prev.username}@${parsed.host}`,
|
||||
name: isGeneratedDisplayName(prev.name, parsed.host, prev.host)
|
||||
? `${parsed.username || prev.username}@${parsed.host}`
|
||||
: prev.name,
|
||||
}))
|
||||
}
|
||||
return
|
||||
@@ -235,12 +247,13 @@ export default function SessionDialog({
|
||||
...prev,
|
||||
host,
|
||||
port: portMatch ? Number(portMatch[1]) : prev.port,
|
||||
name:
|
||||
prev.name.trim() && !prev.name.includes('@') && !prev.name.startsWith('vnc:')
|
||||
? prev.name
|
||||
: formData.protocol === 'vnc'
|
||||
? `vnc:${host}`
|
||||
: `${prev.username || (formData.protocol === 'rdp' ? 'Administrator' : 'root')}@${host}`,
|
||||
name: isGeneratedDisplayName(prev.name, host, prev.host)
|
||||
? defaultDisplayName(
|
||||
formData.protocol,
|
||||
host,
|
||||
prev.username || (formData.protocol === 'rdp' ? 'Administrator' : 'root')
|
||||
)
|
||||
: prev.name,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -320,7 +333,7 @@ export default function SessionDialog({
|
||||
: undefined
|
||||
|
||||
const sessionData = {
|
||||
name: formData.name.trim() || (formData.protocol === 'vnc' ? `vnc:${host}` : `${username}@${host}`),
|
||||
name: formData.name.trim() || defaultDisplayName(formData.protocol, host, username),
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
@@ -436,8 +449,7 @@ export default function SessionDialog({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-[100] flex justify-end">
|
||||
{/* Backdrop */}
|
||||
<AppOverlay open className="z-[100] flex justify-end">
|
||||
<div className="absolute inset-0 animate-fade-in bg-black/45 backdrop-blur-[1px]" onClick={onClose} />
|
||||
|
||||
{/* Drawer */}
|
||||
@@ -450,7 +462,7 @@ export default function SessionDialog({
|
||||
{/* Header */}
|
||||
<div className="border-dark-700/50 flex flex-shrink-0 items-center gap-3 border-b px-4 py-3.5">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-xl border border-accent-blue/20 bg-accent-blue/15 text-accent-blue">
|
||||
<Server className="w-4.5 h-4.5 h-4 w-4" />
|
||||
<Server className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 id="session-drawer-title" className="text-sm font-semibold text-content-primary">
|
||||
@@ -507,7 +519,11 @@ export default function SessionDialog({
|
||||
autoFocus={!isEditing}
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder={t('dialog.displayNamePlaceholder')}
|
||||
placeholder={
|
||||
formData.protocol === 'vnc'
|
||||
? t('dialog.displayNamePlaceholderVnc')
|
||||
: t('dialog.displayNamePlaceholder')
|
||||
}
|
||||
className="ui-input"
|
||||
/>
|
||||
</div>
|
||||
@@ -576,12 +592,13 @@ export default function SessionDialog({
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
username,
|
||||
name:
|
||||
prev.name.trim() && !prev.name.includes('@')
|
||||
? prev.name
|
||||
: prev.host
|
||||
? `${username || (prev.protocol === 'rdp' ? 'Administrator' : 'root')}@${prev.host}`
|
||||
: prev.name,
|
||||
name: isGeneratedDisplayName(prev.name, prev.host)
|
||||
? defaultDisplayName(
|
||||
prev.protocol,
|
||||
prev.host,
|
||||
username || (prev.protocol === 'rdp' ? 'Administrator' : 'root')
|
||||
)
|
||||
: prev.name,
|
||||
}))
|
||||
}}
|
||||
placeholder={
|
||||
@@ -595,20 +612,34 @@ export default function SessionDialog({
|
||||
</div>
|
||||
)}
|
||||
{formData.protocol === 'rdp' && (
|
||||
<div>
|
||||
<label htmlFor="session-password" className="ui-label">
|
||||
{t('dialog.password')}
|
||||
</label>
|
||||
<input
|
||||
id="session-password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
className="ui-input"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div>
|
||||
<label htmlFor="session-password" className="ui-label">
|
||||
{t('dialog.password')}
|
||||
</label>
|
||||
<input
|
||||
id="session-password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
className="ui-input"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="ui-label" htmlFor="session-domain">
|
||||
{t('dialog.domain')}
|
||||
</label>
|
||||
<input
|
||||
id="session-domain"
|
||||
value={formData.domain}
|
||||
onChange={(e) => setFormData({ ...formData, domain: e.target.value })}
|
||||
className="ui-input font-mono"
|
||||
placeholder={t('dialog.domainPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{formData.protocol !== 'ssh' && (
|
||||
<DesktopResolutionPicker
|
||||
@@ -622,168 +653,155 @@ export default function SessionDialog({
|
||||
{formData.protocol === 'rdp' && groupField}
|
||||
</section>
|
||||
|
||||
{/* 凭据 */}
|
||||
<section className="border-dark-700/50 bg-dark-900/30 space-y-3 rounded-xl border p-3.5">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-txt-faint">
|
||||
{t('dialog.login')}
|
||||
</div>
|
||||
{isSsh && (
|
||||
<div className="bg-dark-900/60 border-dark-700/40 grid grid-cols-2 gap-1.5 rounded-xl border p-1">
|
||||
{authTypes.map((type) => {
|
||||
const Icon = type.icon
|
||||
const active = formData.authType === type.value
|
||||
return (
|
||||
<button
|
||||
key={type.value}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, authType: type.value })}
|
||||
className={`flex items-center justify-center gap-1 rounded-lg py-2 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? 'bg-dark-700 text-content-primary shadow-sm'
|
||||
: 'text-txt-muted hover:text-content-secondary'
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{type.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{/* 凭据:SSH / VNC。RDP 的密码和域已在基本区,避免重复。 */}
|
||||
{(isSsh || formData.protocol === 'vnc') && (
|
||||
<section className="border-dark-700/50 bg-dark-900/30 space-y-3 rounded-xl border p-3.5">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-txt-faint">
|
||||
{t('dialog.login')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(formData.authType === 'password' || formData.protocol === 'vnc') && (
|
||||
<div>
|
||||
<label htmlFor="session-password" className="ui-label">
|
||||
{t('dialog.password')}
|
||||
</label>
|
||||
<input
|
||||
id="session-password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
className="ui-input"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.protocol === 'rdp' && (
|
||||
<div>
|
||||
<label className="ui-label" htmlFor="session-domain">
|
||||
{t('dialog.domain')}
|
||||
</label>
|
||||
<input
|
||||
id="session-domain"
|
||||
value={formData.domain}
|
||||
onChange={(e) => setFormData({ ...formData, domain: e.target.value })}
|
||||
className="ui-input font-mono"
|
||||
placeholder={t('dialog.domainPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSsh && formData.authType === 'key' && (
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<div className="ui-label">{t('dialog.keySource')}</div>
|
||||
<div className="bg-dark-900/60 border-dark-700/40 grid grid-cols-2 gap-1.5 rounded-xl border p-1">
|
||||
{(
|
||||
[
|
||||
['file', t('dialog.keyFile')],
|
||||
['text', t('dialog.keyText')],
|
||||
] as const
|
||||
).map(([value, label]) => {
|
||||
const active = formData.keyInputMode === value
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, keyInputMode: value })}
|
||||
className={`flex items-center justify-center rounded-lg py-2 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? 'bg-dark-700 text-content-primary shadow-sm'
|
||||
: 'text-txt-muted hover:text-content-secondary'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.keyInputMode === 'file' ? (
|
||||
<div>
|
||||
<label htmlFor="session-private-key" className="ui-label">
|
||||
{t('dialog.privateKeyPath')}
|
||||
</label>
|
||||
<div className="flex gap-1.5">
|
||||
<input
|
||||
id="session-private-key"
|
||||
type="text"
|
||||
value={formData.privateKeyPath}
|
||||
onChange={(e) => setFormData({ ...formData, privateKeyPath: e.target.value })}
|
||||
placeholder={DEFAULT_KEY_PATH}
|
||||
className="ui-input flex-1"
|
||||
/>
|
||||
{isSsh && (
|
||||
<div className="bg-dark-900/60 border-dark-700/40 grid grid-cols-2 gap-1.5 rounded-xl border p-1">
|
||||
{authTypes.map((type) => {
|
||||
const Icon = type.icon
|
||||
const active = formData.authType === type.value
|
||||
return (
|
||||
<button
|
||||
key={type.value}
|
||||
type="button"
|
||||
className="rounded-lg border border-dark-600 bg-dark-700 px-2.5 text-xs text-content-secondary hover:bg-dark-600"
|
||||
onClick={async () => {
|
||||
const result = await window.opsxApi?.showOpenDialog({
|
||||
title: t('dialog.pickKeyDialogTitle'),
|
||||
filters: [{ name: t('dialog.allFilesFilter'), extensions: ['*'] }],
|
||||
})
|
||||
if (!result?.canceled && result?.filePaths?.[0]) {
|
||||
setFormData({
|
||||
...formData,
|
||||
privateKeyPath: result.filePaths[0],
|
||||
keyInputMode: 'file',
|
||||
})
|
||||
}
|
||||
}}
|
||||
onClick={() => setFormData({ ...formData, authType: type.value })}
|
||||
className={`flex items-center justify-center gap-1 rounded-lg py-2 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? 'bg-dark-700 text-content-primary shadow-sm'
|
||||
: 'text-txt-muted hover:text-content-secondary'
|
||||
}`}
|
||||
>
|
||||
{t('dialog.pickKeyFile')}
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{type.label}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label htmlFor="session-private-key-text" className="ui-label">
|
||||
{t('dialog.privateKeyText')}
|
||||
</label>
|
||||
<textarea
|
||||
id="session-private-key-text"
|
||||
value={formData.privateKeyText}
|
||||
onChange={(e) => setFormData({ ...formData, privateKeyText: e.target.value })}
|
||||
placeholder={'-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----'}
|
||||
rows={6}
|
||||
spellCheck={false}
|
||||
className="ui-input min-h-[7.5rem] resize-y font-mono text-[11px] leading-relaxed"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-txt-faint">{t('dialog.privateKeyTextHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{((isSsh && formData.authType === 'password') || formData.protocol === 'vnc') && (
|
||||
<div>
|
||||
<label htmlFor="session-passphrase" className="ui-label">
|
||||
{t('dialog.passphrase')}
|
||||
<label htmlFor="session-password" className="ui-label">
|
||||
{t('dialog.password')}
|
||||
</label>
|
||||
<input
|
||||
id="session-passphrase"
|
||||
id="session-password"
|
||||
type="password"
|
||||
value={formData.passphrase}
|
||||
onChange={(e) => setFormData({ ...formData, passphrase: e.target.value })}
|
||||
placeholder={t('dialog.passphrasePlaceholder')}
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder="••••••••"
|
||||
className="ui-input"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{(isSsh || formData.protocol === 'vnc') && groupField}
|
||||
</section>
|
||||
{isSsh && formData.authType === 'key' && (
|
||||
<div className="space-y-2.5">
|
||||
<div>
|
||||
<div className="ui-label">{t('dialog.keySource')}</div>
|
||||
<div className="bg-dark-900/60 border-dark-700/40 grid grid-cols-2 gap-1.5 rounded-xl border p-1">
|
||||
{(
|
||||
[
|
||||
['file', t('dialog.keyFile')],
|
||||
['text', t('dialog.keyText')],
|
||||
] as const
|
||||
).map(([value, label]) => {
|
||||
const active = formData.keyInputMode === value
|
||||
return (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setFormData({ ...formData, keyInputMode: value })}
|
||||
className={`flex items-center justify-center rounded-lg py-2 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? 'bg-dark-700 text-content-primary shadow-sm'
|
||||
: 'text-txt-muted hover:text-content-secondary'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{formData.keyInputMode === 'file' ? (
|
||||
<div>
|
||||
<label htmlFor="session-private-key" className="ui-label">
|
||||
{t('dialog.privateKeyPath')}
|
||||
</label>
|
||||
<div className="flex gap-1.5">
|
||||
<input
|
||||
id="session-private-key"
|
||||
type="text"
|
||||
value={formData.privateKeyPath}
|
||||
onChange={(e) => setFormData({ ...formData, privateKeyPath: e.target.value })}
|
||||
placeholder={DEFAULT_KEY_PATH}
|
||||
className="ui-input flex-1"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-dark-600 bg-dark-700 px-2.5 text-xs text-content-secondary hover:bg-dark-600"
|
||||
onClick={async () => {
|
||||
const result = await window.opsxApi?.showOpenDialog({
|
||||
title: t('dialog.pickKeyDialogTitle'),
|
||||
filters: [{ name: t('dialog.allFilesFilter'), extensions: ['*'] }],
|
||||
})
|
||||
if (!result?.canceled && result?.filePaths?.[0]) {
|
||||
setFormData({
|
||||
...formData,
|
||||
privateKeyPath: result.filePaths[0],
|
||||
keyInputMode: 'file',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('dialog.pickKeyFile')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<label htmlFor="session-private-key-text" className="ui-label">
|
||||
{t('dialog.privateKeyText')}
|
||||
</label>
|
||||
<textarea
|
||||
id="session-private-key-text"
|
||||
value={formData.privateKeyText}
|
||||
onChange={(e) => setFormData({ ...formData, privateKeyText: e.target.value })}
|
||||
placeholder={'-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----'}
|
||||
rows={6}
|
||||
spellCheck={false}
|
||||
className="ui-input min-h-[7.5rem] resize-y font-mono text-[11px] leading-relaxed"
|
||||
/>
|
||||
<p className="mt-1 text-[10px] text-txt-faint">{t('dialog.privateKeyTextHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="session-passphrase" className="ui-label">
|
||||
{t('dialog.passphrase')}
|
||||
</label>
|
||||
<input
|
||||
id="session-passphrase"
|
||||
type="password"
|
||||
value={formData.passphrase}
|
||||
onChange={(e) => setFormData({ ...formData, passphrase: e.target.value })}
|
||||
placeholder={t('dialog.passphrasePlaceholder')}
|
||||
className="ui-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupField}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* 高级 */}
|
||||
<div className="border-dark-700/50 overflow-hidden rounded-xl border">
|
||||
@@ -794,7 +812,7 @@ export default function SessionDialog({
|
||||
>
|
||||
<span className="text-xs font-semibold text-content-secondary">{t('dialog.advanced')}</span>
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-txt-faint">
|
||||
{showAdvanced ? t('dialog.collapse') : t('dialog.advancedCollapsed')}
|
||||
{showAdvanced ? t('dialog.collapse') : isSsh ? t('dialog.advancedCollapsed') : t('dialog.tags')}
|
||||
<ChevronDown className={`h-3.5 w-3.5 transition-transform ${showAdvanced ? 'rotate-180' : ''}`} />
|
||||
</span>
|
||||
</button>
|
||||
@@ -817,146 +835,150 @@ export default function SessionDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="ui-label">{t('dialog.startupCommand')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.startupCommand}
|
||||
onChange={(e) => setFormData({ ...formData, startupCommand: e.target.value })}
|
||||
placeholder={t('dialog.startupCommandPlaceholder')}
|
||||
className="ui-input-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
{isSsh && (
|
||||
<>
|
||||
<div>
|
||||
<label className="ui-label">{t('dialog.startupCommand')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.startupCommand}
|
||||
onChange={(e) => setFormData({ ...formData, startupCommand: e.target.value })}
|
||||
placeholder={t('dialog.startupCommandPlaceholder')}
|
||||
className="ui-input-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="ui-label">{t('dialog.proxy')}</label>
|
||||
<div className="mb-2 flex gap-1">
|
||||
{(
|
||||
[
|
||||
{ value: 'none', label: t('dialog.proxyNone') },
|
||||
{ value: 'http', label: 'HTTP' },
|
||||
{ value: 'socks5', label: 'SOCKS5' },
|
||||
] as const
|
||||
).map((t) => (
|
||||
<button
|
||||
key={t.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (t.value !== 'none' && !useEntitlementStore.getState().requirePro('proxy')) {
|
||||
return
|
||||
}
|
||||
<div>
|
||||
<label className="ui-label">{t('dialog.proxy')}</label>
|
||||
<div className="mb-2 flex gap-1">
|
||||
{(
|
||||
[
|
||||
{ value: 'none', label: t('dialog.proxyNone') },
|
||||
{ value: 'http', label: 'HTTP' },
|
||||
{ value: 'socks5', label: 'SOCKS5' },
|
||||
] as const
|
||||
).map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (option.value !== 'none' && !useEntitlementStore.getState().requirePro('proxy')) {
|
||||
return
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: {
|
||||
...formData.proxy,
|
||||
type: option.value,
|
||||
port:
|
||||
formData.proxy.port ||
|
||||
(option.value === 'http' ? 8080 : option.value === 'socks5' ? 1080 : 0),
|
||||
},
|
||||
jumpSessionIds: option.value === 'none' ? formData.jumpSessionIds : [],
|
||||
})
|
||||
}}
|
||||
className={`flex-1 rounded-lg border py-1.5 text-[11px] transition-colors ${
|
||||
formData.proxy.type === option.value
|
||||
? 'border-accent-blue bg-accent-blue/15 text-accent-blue'
|
||||
: 'border-dark-600 text-txt-muted hover:border-dark-500'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{formData.proxy.type !== 'none' && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.host}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, host: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyHostPlaceholder')}
|
||||
className="ui-input-sm col-span-2"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.proxy.port || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: {
|
||||
...formData.proxy,
|
||||
port: parseInt(e.target.value) || 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyPortPlaceholder')}
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.username}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, username: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyUserPlaceholder')}
|
||||
autoComplete="off"
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.proxy.password}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, password: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyPasswordPlaceholder')}
|
||||
autoComplete="new-password"
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="session-jump-host" className="ui-label">
|
||||
{t('dialog.jumpHost')}
|
||||
</label>
|
||||
<select
|
||||
id="session-jump-host"
|
||||
value={formData.jumpSessionIds[0] || ''}
|
||||
onChange={(event) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: {
|
||||
...formData.proxy,
|
||||
type: t.value,
|
||||
port:
|
||||
formData.proxy.port ||
|
||||
(t.value === 'http' ? 8080 : t.value === 'socks5' ? 1080 : 0),
|
||||
},
|
||||
jumpSessionIds: t.value === 'none' ? formData.jumpSessionIds : [],
|
||||
jumpSessionIds: event.target.value ? [event.target.value] : [],
|
||||
proxy: event.target.value
|
||||
? { type: 'none', host: '', port: 0, username: '', password: '' }
|
||||
: formData.proxy,
|
||||
})
|
||||
}}
|
||||
className={`flex-1 rounded-lg border py-1.5 text-[11px] transition-colors ${
|
||||
formData.proxy.type === t.value
|
||||
? 'border-accent-blue bg-accent-blue/15 text-accent-blue'
|
||||
: 'border-dark-600 text-txt-muted hover:border-dark-500'
|
||||
}`}
|
||||
}
|
||||
className="ui-input-sm"
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{formData.proxy.type !== 'none' && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="grid grid-cols-3 gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.host}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, host: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyHostPlaceholder')}
|
||||
className="ui-input-sm col-span-2"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.proxy.port || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: {
|
||||
...formData.proxy,
|
||||
port: parseInt(e.target.value) || 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyPortPlaceholder')}
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<input
|
||||
type="text"
|
||||
value={formData.proxy.username}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, username: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyUserPlaceholder')}
|
||||
autoComplete="off"
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.proxy.password}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
proxy: { ...formData.proxy, password: e.target.value },
|
||||
})
|
||||
}
|
||||
placeholder={t('dialog.proxyPasswordPlaceholder')}
|
||||
autoComplete="new-password"
|
||||
className="ui-input-sm"
|
||||
/>
|
||||
</div>
|
||||
<option value="">{t('dialog.jumpNoneAlt')}</option>
|
||||
{jumpCandidates.map((session) => (
|
||||
<option key={session.id} value={session.id}>
|
||||
{session.name} ({session.username}@{session.host})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-[10px] text-txt-faint">{t('dialog.jumpHint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="session-jump-host" className="ui-label">
|
||||
{t('dialog.jumpHost')}
|
||||
</label>
|
||||
<select
|
||||
id="session-jump-host"
|
||||
value={formData.jumpSessionIds[0] || ''}
|
||||
onChange={(event) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
jumpSessionIds: event.target.value ? [event.target.value] : [],
|
||||
proxy: event.target.value
|
||||
? { type: 'none', host: '', port: 0, username: '', password: '' }
|
||||
: formData.proxy,
|
||||
})
|
||||
}
|
||||
className="ui-input-sm"
|
||||
>
|
||||
<option value="">{t('dialog.jumpNoneAlt')}</option>
|
||||
{jumpCandidates.map((session) => (
|
||||
<option key={session.id} value={session.id}>
|
||||
{session.name} ({session.username}@{session.host})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-[10px] text-txt-faint">{t('dialog.jumpHint')}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -985,7 +1007,7 @@ export default function SessionDialog({
|
||||
</div>
|
||||
</form>
|
||||
</aside>
|
||||
</div>
|
||||
</AppOverlay>
|
||||
{PromptDialogComponent}
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user