Feat: Allow HTTP proxy authentication inputs (#4479)
* sub dir * setting proxy * test useConfigurations * fix lint * test * test 2 * update check
This commit is contained in:
parent
8c6e6edd36
commit
83550cd0d1
@ -5,8 +5,15 @@ const EXPERIMENTAL_FEATURE = 'experimentalFeature'
|
||||
const PROXY_FEATURE_ENABLED = 'proxyFeatureEnabled'
|
||||
const VULKAN_ENABLED = 'vulkanEnabled'
|
||||
const IGNORE_SSL = 'ignoreSSLFeature'
|
||||
const VERIFY_PROXY_SSL = 'verifyProxySSL'
|
||||
const VERIFY_PROXY_HOST_SSL = 'verifyProxyHostSSL'
|
||||
const VERIFY_PEER_SSL = 'verifyPeerSSL'
|
||||
const VERIFY_HOST_SSL = 'verifyHostSSL'
|
||||
const HTTPS_PROXY_FEATURE = 'httpsProxyFeature'
|
||||
const PROXY_USERNAME = 'proxyUsername'
|
||||
const PROXY_PASSWORD = 'proxyPassword'
|
||||
const QUICK_ASK_ENABLED = 'quickAskEnabled'
|
||||
const NO_PROXY = 'noProxy'
|
||||
|
||||
export const janDataFolderPathAtom = atom('')
|
||||
|
||||
@ -27,9 +34,56 @@ export const proxyAtom = atomWithStorage(HTTPS_PROXY_FEATURE, '', undefined, {
|
||||
getOnInit: true,
|
||||
})
|
||||
|
||||
export const proxyUsernameAtom = atomWithStorage(
|
||||
PROXY_USERNAME,
|
||||
'',
|
||||
undefined,
|
||||
{ getOnInit: true }
|
||||
)
|
||||
|
||||
export const proxyPasswordAtom = atomWithStorage(
|
||||
PROXY_PASSWORD,
|
||||
'',
|
||||
undefined,
|
||||
{ getOnInit: true }
|
||||
)
|
||||
|
||||
export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false, undefined, {
|
||||
getOnInit: true,
|
||||
})
|
||||
|
||||
export const noProxyAtom = atomWithStorage(NO_PROXY, '', undefined, {
|
||||
getOnInit: false,
|
||||
})
|
||||
|
||||
export const verifyProxySslAtom = atomWithStorage(
|
||||
VERIFY_PROXY_SSL,
|
||||
false,
|
||||
undefined,
|
||||
{ getOnInit: true }
|
||||
)
|
||||
|
||||
export const verifyProxyHostSslAtom = atomWithStorage(
|
||||
VERIFY_PROXY_HOST_SSL,
|
||||
false,
|
||||
undefined,
|
||||
{ getOnInit: true }
|
||||
)
|
||||
|
||||
export const verifyPeerSslAtom = atomWithStorage(
|
||||
VERIFY_PEER_SSL,
|
||||
false,
|
||||
undefined,
|
||||
{ getOnInit: true }
|
||||
)
|
||||
|
||||
export const verifyHostSslAtom = atomWithStorage(
|
||||
VERIFY_HOST_SSL,
|
||||
false,
|
||||
undefined,
|
||||
{ getOnInit: true }
|
||||
)
|
||||
|
||||
export const vulkanEnabledAtom = atomWithStorage(
|
||||
VULKAN_ENABLED,
|
||||
false,
|
||||
|
||||
137
web/hooks/useConfigurations.test.ts
Normal file
137
web/hooks/useConfigurations.test.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { renderHook, act } from '@testing-library/react'
|
||||
import { useConfigurations } from './useConfigurations'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { extensionManager } from '@/extension'
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('jotai', () => {
|
||||
const originalModule = jest.requireActual('jotai')
|
||||
return {
|
||||
...originalModule,
|
||||
useAtomValue: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/extension', () => ({
|
||||
extensionManager: {
|
||||
get: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
describe('useConfigurations', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should call configurePullOptions with correct proxy settings when proxy is enabled', () => {
|
||||
// Explicitly set mock return values for each call
|
||||
(useAtomValue as jest.Mock)
|
||||
.mockReturnValueOnce(true) // proxyEnabled
|
||||
.mockReturnValueOnce('http://proxy.example.com') // proxyUrl
|
||||
.mockReturnValueOnce('') // proxyIgnoreSSL
|
||||
.mockReturnValueOnce(true) // verifyProxySSL
|
||||
.mockReturnValueOnce(true) // verifyProxyHostSSL
|
||||
.mockReturnValueOnce(true) // verifyPeerSSL
|
||||
.mockReturnValueOnce(true) // verifyHostSSL
|
||||
.mockReturnValueOnce('') // noProxy
|
||||
.mockReturnValueOnce('username') // proxyUsername
|
||||
.mockReturnValueOnce('password') // proxyPassword
|
||||
|
||||
|
||||
const mockConfigurePullOptions = jest.fn()
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||
configurePullOptions: mockConfigurePullOptions,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useConfigurations())
|
||||
|
||||
act(() => {
|
||||
result.current.configurePullOptions()
|
||||
})
|
||||
|
||||
expect(mockConfigurePullOptions).toHaveBeenCalledWith({
|
||||
proxy_username: 'username',
|
||||
proxy_password: 'password',
|
||||
proxy_url: 'http://proxy.example.com',
|
||||
verify_proxy_ssl: true,
|
||||
verify_proxy_host_ssl: true,
|
||||
verify_peer_ssl: true,
|
||||
verify_host_ssl: true,
|
||||
no_proxy: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('should call configurePullOptions with empty proxy settings when proxy is disabled', () => {
|
||||
// Mock atom values
|
||||
;(useAtomValue as jest.Mock)
|
||||
.mockReturnValueOnce(false) // proxyEnabled
|
||||
.mockReturnValueOnce('') // proxyUrl
|
||||
.mockReturnValueOnce(false) // proxyIgnoreSSL
|
||||
.mockReturnValueOnce('') // noProxy
|
||||
.mockReturnValueOnce('') // proxyUsername
|
||||
.mockReturnValueOnce('') // proxyPassword
|
||||
.mockReturnValueOnce(false) // verifyProxySSL
|
||||
.mockReturnValueOnce(false) // verifyProxyHostSSL
|
||||
.mockReturnValueOnce(false) // verifyPeerSSL
|
||||
.mockReturnValueOnce(false) // verifyHostSSL
|
||||
|
||||
const mockConfigurePullOptions = jest.fn()
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||
configurePullOptions: mockConfigurePullOptions,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useConfigurations())
|
||||
|
||||
act(() => {
|
||||
result.current.configurePullOptions()
|
||||
})
|
||||
|
||||
expect(mockConfigurePullOptions).toHaveBeenCalledWith({
|
||||
proxy_username: '',
|
||||
proxy_password: '',
|
||||
proxy_url: '',
|
||||
verify_proxy_ssl: false,
|
||||
verify_proxy_host_ssl: false,
|
||||
verify_peer_ssl: false,
|
||||
verify_host_ssl: false,
|
||||
no_proxy: '',
|
||||
})
|
||||
})
|
||||
|
||||
it('should set all verify SSL to false when proxyIgnoreSSL is true', () => {
|
||||
// Mock atom values
|
||||
;(useAtomValue as jest.Mock)
|
||||
.mockReturnValueOnce(true) // proxyEnabled
|
||||
.mockReturnValueOnce('http://proxy.example.com') // proxyUrl
|
||||
.mockReturnValueOnce(true) // proxyIgnoreSSL
|
||||
.mockReturnValueOnce(true) // verifyProxySSL
|
||||
.mockReturnValueOnce(true) // verifyProxyHostSSL
|
||||
.mockReturnValueOnce(true) // verifyPeerSSL
|
||||
.mockReturnValueOnce(true) // verifyHostSSL
|
||||
.mockReturnValueOnce('') // noProxy
|
||||
.mockReturnValueOnce('username') // proxyUsername
|
||||
.mockReturnValueOnce('password') // proxyPassword
|
||||
|
||||
const mockConfigurePullOptions = jest.fn()
|
||||
;(extensionManager.get as jest.Mock).mockReturnValue({
|
||||
configurePullOptions: mockConfigurePullOptions,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useConfigurations())
|
||||
|
||||
act(() => {
|
||||
result.current.configurePullOptions()
|
||||
})
|
||||
|
||||
expect(mockConfigurePullOptions).toHaveBeenCalledWith({
|
||||
proxy_username: 'username',
|
||||
proxy_password: 'password',
|
||||
proxy_url: 'http://proxy.example.com',
|
||||
verify_proxy_ssl: false,
|
||||
verify_proxy_host_ssl: false,
|
||||
verify_peer_ssl: false,
|
||||
verify_host_ssl: false,
|
||||
no_proxy: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -6,14 +6,28 @@ import { useAtomValue } from 'jotai'
|
||||
import { extensionManager } from '@/extension'
|
||||
import {
|
||||
ignoreSslAtom,
|
||||
noProxyAtom,
|
||||
proxyAtom,
|
||||
proxyEnabledAtom,
|
||||
proxyPasswordAtom,
|
||||
proxyUsernameAtom,
|
||||
verifyHostSslAtom,
|
||||
verifyPeerSslAtom,
|
||||
verifyProxyHostSslAtom,
|
||||
verifyProxySslAtom,
|
||||
} from '@/helpers/atoms/AppConfig.atom'
|
||||
|
||||
export const useConfigurations = () => {
|
||||
const proxyEnabled = useAtomValue(proxyEnabledAtom)
|
||||
const proxyUrl = useAtomValue(proxyAtom)
|
||||
const proxyIgnoreSSL = useAtomValue(ignoreSslAtom)
|
||||
const verifyProxySSL = useAtomValue(verifyProxySslAtom)
|
||||
const verifyProxyHostSSL = useAtomValue(verifyProxyHostSslAtom)
|
||||
const verifyPeerSSL = useAtomValue(verifyPeerSslAtom)
|
||||
const verifyHostSSL = useAtomValue(verifyHostSslAtom)
|
||||
const noProxy = useAtomValue(noProxyAtom)
|
||||
const proxyUsername = useAtomValue(proxyUsernameAtom)
|
||||
const proxyPassword = useAtomValue(proxyPasswordAtom)
|
||||
|
||||
const configurePullOptions = useCallback(() => {
|
||||
extensionManager
|
||||
@ -21,20 +35,45 @@ export const useConfigurations = () => {
|
||||
?.configurePullOptions(
|
||||
proxyEnabled
|
||||
? {
|
||||
proxy_username: proxyUsername,
|
||||
proxy_password: proxyPassword,
|
||||
proxy_url: proxyUrl,
|
||||
verify_peer_ssl: !proxyIgnoreSSL,
|
||||
verify_proxy_ssl: proxyIgnoreSSL ? false : verifyProxySSL,
|
||||
verify_proxy_host_ssl: proxyIgnoreSSL
|
||||
? false
|
||||
: verifyProxyHostSSL,
|
||||
verify_peer_ssl: proxyIgnoreSSL ? false : verifyPeerSSL,
|
||||
verify_host_ssl: proxyIgnoreSSL ? false : verifyHostSSL,
|
||||
no_proxy: noProxy,
|
||||
}
|
||||
: {
|
||||
proxy_username: '',
|
||||
proxy_password: '',
|
||||
proxy_url: '',
|
||||
verify_proxy_ssl: false,
|
||||
verify_proxy_host_ssl: false,
|
||||
verify_peer_ssl: false,
|
||||
verify_host_ssl: false,
|
||||
no_proxy: '',
|
||||
}
|
||||
)
|
||||
}, [proxyEnabled, proxyUrl, proxyIgnoreSSL])
|
||||
}, [
|
||||
proxyEnabled,
|
||||
proxyUrl,
|
||||
proxyIgnoreSSL,
|
||||
noProxy,
|
||||
proxyUsername,
|
||||
proxyPassword,
|
||||
verifyProxySSL,
|
||||
verifyProxyHostSSL,
|
||||
verifyPeerSSL,
|
||||
verifyHostSSL,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
configurePullOptions()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, [configurePullOptions])
|
||||
|
||||
return {
|
||||
configurePullOptions,
|
||||
|
||||
147
web/screens/Settings/Advanced/ProxySettings/index.test.tsx
Normal file
147
web/screens/Settings/Advanced/ProxySettings/index.test.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import ProxySettings from '.'
|
||||
|
||||
// Mock ResizeObserver
|
||||
class ResizeObserverMock {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
global.ResizeObserver = ResizeObserverMock as any
|
||||
|
||||
// Mock global window.core
|
||||
global.window.core = {
|
||||
api: {
|
||||
getAppConfigurations: () => jest.fn(),
|
||||
updateAppConfiguration: () => jest.fn(),
|
||||
relaunch: () => jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('@/hooks/useConfigurations', () => ({
|
||||
useConfigurations: () => ({
|
||||
configurePullOptions: jest.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('jotai', () => {
|
||||
const originalModule = jest.requireActual('jotai')
|
||||
return {
|
||||
...originalModule,
|
||||
useAtom: jest.fn().mockImplementation((atom) => {
|
||||
switch (atom) {
|
||||
case 'proxyEnabledAtom':
|
||||
return [true, jest.fn()]
|
||||
case 'proxyAtom':
|
||||
return ['', jest.fn()]
|
||||
case 'proxyUsernameAtom':
|
||||
return ['', jest.fn()]
|
||||
case 'proxyPasswordAtom':
|
||||
return ['', jest.fn()]
|
||||
case 'ignoreSslAtom':
|
||||
return [false, jest.fn()]
|
||||
case 'verifyProxySslAtom':
|
||||
return [true, jest.fn()]
|
||||
case 'verifyProxyHostSslAtom':
|
||||
return [true, jest.fn()]
|
||||
case 'verifyPeerSslAtom':
|
||||
return [true, jest.fn()]
|
||||
case 'verifyHostSslAtom':
|
||||
return [true, jest.fn()]
|
||||
case 'noProxyAtom':
|
||||
return ['localhost', jest.fn()]
|
||||
default:
|
||||
return [null, jest.fn()]
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('ProxySettings', () => {
|
||||
const mockOnBack = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders the component', async () => {
|
||||
render(<ProxySettings onBack={mockOnBack} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument()
|
||||
expect(screen.getByText('Proxy URL')).toBeInTheDocument()
|
||||
expect(screen.getByText('Authentication')).toBeInTheDocument()
|
||||
expect(screen.getByText('No Proxy')).toBeInTheDocument()
|
||||
expect(screen.getByText('SSL Verification')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('handles back navigation', async () => {
|
||||
render(<ProxySettings onBack={mockOnBack} />)
|
||||
|
||||
const backButton = screen.getByText('Advanced Settings')
|
||||
fireEvent.click(backButton)
|
||||
|
||||
expect(mockOnBack).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('toggles password visibility', () => {
|
||||
render(<ProxySettings onBack={mockOnBack} />)
|
||||
|
||||
const passwordVisibilityToggle = screen.getByTestId('password-visibility-toggle')
|
||||
const passwordInput = screen.getByTestId('proxy-password')
|
||||
|
||||
expect(passwordInput).toHaveAttribute('type', 'password')
|
||||
|
||||
fireEvent.click(passwordVisibilityToggle)
|
||||
expect(passwordInput).toHaveAttribute('type', 'text')
|
||||
|
||||
fireEvent.click(passwordVisibilityToggle)
|
||||
expect(passwordInput).toHaveAttribute('type', 'password')
|
||||
})
|
||||
|
||||
it('allows clearing input fields', async () => {
|
||||
render(<ProxySettings onBack={mockOnBack} />)
|
||||
|
||||
// Test clearing proxy URL
|
||||
const proxyInput = screen.getByTestId('proxy-input')
|
||||
fireEvent.change(proxyInput, { target: { value: 'http://test.proxy' } })
|
||||
|
||||
const clearProxyButton = screen.getByTestId('clear-proxy-button')
|
||||
fireEvent.click(clearProxyButton)
|
||||
expect(proxyInput).toHaveValue('')
|
||||
|
||||
// Test clearing username
|
||||
const usernameInput = screen.getByTestId('proxy-username')
|
||||
fireEvent.change(usernameInput, { target: { value: 'testuser' } })
|
||||
|
||||
// Test clearing password
|
||||
const passwordInput = screen.getByTestId('proxy-password')
|
||||
fireEvent.change(passwordInput, { target: { value: 'testpassword' } })
|
||||
})
|
||||
|
||||
it('renders SSL verification switches', async () => {
|
||||
render(<ProxySettings onBack={mockOnBack} />)
|
||||
|
||||
const sslSwitches = [
|
||||
'Ignore SSL certificates',
|
||||
'Verify Proxy SSL',
|
||||
'Verify Proxy Host SSL',
|
||||
'Verify Peer SSL',
|
||||
'Verify Host SSL'
|
||||
]
|
||||
|
||||
sslSwitches.forEach(switchText => {
|
||||
const switchElement = screen.getByText(switchText)
|
||||
expect(switchElement).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
394
web/screens/Settings/Advanced/ProxySettings/index.tsx
Normal file
394
web/screens/Settings/Advanced/ProxySettings/index.tsx
Normal file
@ -0,0 +1,394 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { Input, ScrollArea, Switch } from '@janhq/joi'
|
||||
import { useAtom } from 'jotai'
|
||||
import { EyeIcon, EyeOffIcon, XIcon, ArrowLeftIcon } from 'lucide-react'
|
||||
import { useDebouncedCallback } from 'use-debounce'
|
||||
|
||||
import { useConfigurations } from '@/hooks/useConfigurations'
|
||||
|
||||
import {
|
||||
ignoreSslAtom,
|
||||
proxyAtom,
|
||||
proxyEnabledAtom,
|
||||
verifyProxySslAtom,
|
||||
verifyProxyHostSslAtom,
|
||||
verifyPeerSslAtom,
|
||||
verifyHostSslAtom,
|
||||
noProxyAtom,
|
||||
proxyUsernameAtom,
|
||||
proxyPasswordAtom,
|
||||
} from '@/helpers/atoms/AppConfig.atom'
|
||||
|
||||
const ProxySettings = ({ onBack }: { onBack: () => void }) => {
|
||||
const [proxyEnabled] = useAtom(proxyEnabledAtom)
|
||||
const [proxy, setProxy] = useAtom(proxyAtom)
|
||||
const [noProxy, setNoProxy] = useAtom(noProxyAtom)
|
||||
const [partialProxy, setPartialProxy] = useState<string>(proxy)
|
||||
const [proxyUsername, setProxyUsername] = useAtom(proxyUsernameAtom)
|
||||
const [proxyPassword, setProxyPassword] = useAtom(proxyPasswordAtom)
|
||||
const [proxyPartialPassword, setProxyPartialPassword] =
|
||||
useState<string>(proxyPassword)
|
||||
const [proxyPartialUsername, setProxyPartialUsername] =
|
||||
useState<string>(proxyUsername)
|
||||
const { configurePullOptions } = useConfigurations()
|
||||
const [ignoreSSL, setIgnoreSSL] = useAtom(ignoreSslAtom)
|
||||
const [verifyProxySSL, setVerifyProxySSL] = useAtom(verifyProxySslAtom)
|
||||
const [verifyProxyHostSSL, setVerifyProxyHostSSL] = useAtom(
|
||||
verifyProxyHostSslAtom
|
||||
)
|
||||
const [verifyPeerSSL, setVerifyPeerSSL] = useAtom(verifyPeerSslAtom)
|
||||
const [verifyHostSSL, setVerifyHostSSL] = useAtom(verifyHostSslAtom)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const updatePullOptions = useDebouncedCallback(
|
||||
() => configurePullOptions(),
|
||||
1000
|
||||
)
|
||||
|
||||
const onProxyChange = useDebouncedCallback((value: string) => {
|
||||
if (value.trim().startsWith('http')) {
|
||||
setProxy(value.trim())
|
||||
updatePullOptions()
|
||||
} else {
|
||||
setProxy('')
|
||||
updatePullOptions()
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const onProxyUsernameChange = useDebouncedCallback((value: string) => {
|
||||
setProxyUsername(value)
|
||||
updatePullOptions()
|
||||
}, 1000)
|
||||
|
||||
const onProxyPasswordChange = useDebouncedCallback((value: string) => {
|
||||
setProxyPassword(value)
|
||||
updatePullOptions()
|
||||
}, 1000)
|
||||
|
||||
const handleProxyInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value || ''
|
||||
setPartialProxy(value)
|
||||
onProxyChange(value)
|
||||
},
|
||||
[setPartialProxy, onProxyChange]
|
||||
)
|
||||
|
||||
const handleProxyUsernameInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value || ''
|
||||
setProxyPartialUsername(value)
|
||||
onProxyUsernameChange(value)
|
||||
},
|
||||
[setProxyPartialUsername, onProxyUsernameChange]
|
||||
)
|
||||
|
||||
const handleProxyPasswordInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value || ''
|
||||
setProxyPartialPassword(value)
|
||||
onProxyPasswordChange(value)
|
||||
},
|
||||
[setProxyPartialPassword, onProxyPasswordChange]
|
||||
)
|
||||
|
||||
const onNoProxyChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const listNoProxy = e.target.value || ''
|
||||
const listNoProxyTrim = listNoProxy.split(',').map((item) => item.trim())
|
||||
setNoProxy(listNoProxyTrim.join(','))
|
||||
updatePullOptions()
|
||||
},
|
||||
[setNoProxy, updatePullOptions]
|
||||
)
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full w-full">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 z-10 flex h-12 items-center border-b border-[hsla(var(--app-border))] bg-[hsla(var(--app-bg))] px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="flex items-center gap-1 text-sm text-[hsla(var(--text-secondary))] hover:text-[hsla(var(--text-primary))]"
|
||||
>
|
||||
<ArrowLeftIcon size={16} />
|
||||
<span>Advanced Settings</span>
|
||||
</button>
|
||||
<span className="text-sm text-[hsla(var(--text-secondary))]">/</span>
|
||||
<span className="text-sm">
|
||||
<strong>HTTPS Proxy</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold">Proxy Configuration</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="w-full space-y-1">
|
||||
<label className="text-sm font-medium">Proxy URL</label>
|
||||
<p className="text-xs text-[hsla(var(--text-secondary))]">
|
||||
URL and port of your proxy server.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-shrink-0 flex-col items-end gap-2 pr-1 sm:w-1/2">
|
||||
<div className="w-full">
|
||||
<Input
|
||||
data-testid="proxy-input"
|
||||
placeholder="http://<user>:<password>@<domain or IP>:<port>"
|
||||
value={partialProxy}
|
||||
onChange={handleProxyInputChange}
|
||||
suffixIcon={
|
||||
<div className="flex items-center gap-1">
|
||||
{partialProxy && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="clear-proxy-button"
|
||||
onClick={() => {
|
||||
setPartialProxy('')
|
||||
setProxy('')
|
||||
}}
|
||||
className="p-1 hover:text-[hsla(var(--text-primary))]"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">Authentication</label>
|
||||
<p className="text-xs text-[hsla(var(--text-secondary))]">
|
||||
Credentials for your proxy server (if required).
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-1/2 space-y-2">
|
||||
<Input
|
||||
data-testid="proxy-username"
|
||||
placeholder="Username"
|
||||
value={proxyPartialUsername}
|
||||
onChange={handleProxyUsernameInputChange}
|
||||
suffixIcon={
|
||||
<div className="flex items-center gap-1">
|
||||
{proxyUsername && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="clear-username-button"
|
||||
onClick={() => setProxyUsername('')}
|
||||
className="p-1 hover:text-[hsla(var(--text-primary))]"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
data-testid="proxy-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Password"
|
||||
value={proxyPartialPassword}
|
||||
onChange={handleProxyPasswordInputChange}
|
||||
suffixIcon={
|
||||
<div className="flex items-center gap-1">
|
||||
{proxyPassword && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="clear-password-button"
|
||||
onClick={() => {
|
||||
setProxyPassword('')
|
||||
}}
|
||||
className="p-1 hover:text-[hsla(var(--text-primary))]"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
data-testid="password-visibility-toggle"
|
||||
className="p-1 hover:text-[hsla(var(--text-primary))]"
|
||||
type="button"
|
||||
aria-label="Toggle password visibility"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon size={14} />
|
||||
) : (
|
||||
<EyeIcon size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* No Proxy */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium">No Proxy</label>
|
||||
<p className="text-xs text-[hsla(var(--text-secondary))]">
|
||||
List of hosts that should bypass the proxy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Input
|
||||
data-testid="no-proxy-input"
|
||||
placeholder="localhost, 127.0.0.1"
|
||||
value={noProxy}
|
||||
onChange={onNoProxyChange}
|
||||
suffixIcon={
|
||||
<div className="flex items-center gap-1">
|
||||
{noProxy && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setNoProxy('')}
|
||||
className="p-1 hover:text-[hsla(var(--text-primary))]"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold">SSL Verification</h2>
|
||||
</div>
|
||||
|
||||
{/* Ignore SSL certificates */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="max-w-[66%] flex-shrink-0 space-y-1">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">
|
||||
Ignore SSL certificates
|
||||
</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Allow self-signed or unverified certificates (may be required
|
||||
for certain proxies). Enable this reduces security. Only use
|
||||
this if you trust your proxy server.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="ignore-ssl-switch"
|
||||
checked={ignoreSSL}
|
||||
onChange={(e) => {
|
||||
setIgnoreSSL(e.target.checked)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Verify Proxy SSL */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="flex-shrink-0 space-y-1">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">Verify Proxy SSL</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Validate SSL certificate when connecting to the proxy server.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="verify-proxy-ssl-switch"
|
||||
checked={verifyProxySSL}
|
||||
onChange={(e) => {
|
||||
setVerifyProxySSL(e.target.checked)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Verify Proxy Host SSL */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="flex-shrink-0 space-y-1">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">
|
||||
Verify Proxy Host SSL
|
||||
</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Validate SSL certificate of the proxy server host.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="verify-proxy-host-ssl-switch"
|
||||
checked={verifyProxyHostSSL}
|
||||
onChange={(e) => {
|
||||
setVerifyProxyHostSSL(e.target.checked)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Verify Peer SSL */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="flex-shrink-0 space-y-1">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">Verify Peer SSL</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Validate SSL certificate of the peer connections.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="verify-peer-ssl-switch"
|
||||
checked={verifyPeerSSL}
|
||||
onChange={(e) => {
|
||||
setVerifyPeerSSL(e.target.checked)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Verify Host SSL */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="flex-shrink-0 space-y-1">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">Verify Host SSL</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Validate SSL certificate of destination hosts.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="verify-host-ssl-switch"
|
||||
checked={verifyHostSSL}
|
||||
onChange={(e) => {
|
||||
setVerifyHostSSL(e.target.checked)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProxySettings
|
||||
@ -64,7 +64,6 @@ describe('Advanced', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Experimental Mode')).toBeInTheDocument()
|
||||
expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument()
|
||||
expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument()
|
||||
expect(screen.getByText('Jan Data Folder')).toBeInTheDocument()
|
||||
expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument()
|
||||
})
|
||||
@ -102,28 +101,6 @@ describe('Advanced', () => {
|
||||
expect(proxyToggle).toBeChecked()
|
||||
})
|
||||
|
||||
it('updates proxy settings', async () => {
|
||||
render(<Advanced />)
|
||||
let proxyInput
|
||||
await waitFor(() => {
|
||||
const proxyToggle = screen.getByTestId(/proxy-switch/i)
|
||||
fireEvent.click(proxyToggle)
|
||||
proxyInput = screen.getByTestId(/proxy-input/i)
|
||||
fireEvent.change(proxyInput, { target: { value: 'http://proxy.com' } })
|
||||
})
|
||||
expect(proxyInput).toHaveValue('http://proxy.com')
|
||||
})
|
||||
|
||||
it('toggles ignore SSL certificates', async () => {
|
||||
render(<Advanced />)
|
||||
let ignoreSslToggle
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument()
|
||||
ignoreSslToggle = screen.getByTestId(/ignore-ssl-switch/i)
|
||||
fireEvent.click(ignoreSslToggle)
|
||||
})
|
||||
expect(ignoreSslToggle).toBeChecked()
|
||||
})
|
||||
|
||||
it('renders DataFolder component', async () => {
|
||||
render(<Advanced />)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useCallback, ChangeEvent } from 'react'
|
||||
import { useEffect, useState, ChangeEvent } from 'react'
|
||||
|
||||
import { openExternalUrl, AppConfiguration } from '@janhq/core'
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '@janhq/joi'
|
||||
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||
import { ChevronDownIcon } from 'lucide-react'
|
||||
import { ChevronDownIcon, ArrowRightIcon } from 'lucide-react'
|
||||
import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react'
|
||||
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@ -35,8 +35,6 @@ import FactoryReset from './FactoryReset'
|
||||
|
||||
import {
|
||||
experimentalFeatureEnabledAtom,
|
||||
ignoreSslAtom,
|
||||
proxyAtom,
|
||||
proxyEnabledAtom,
|
||||
vulkanEnabledAtom,
|
||||
quickAskEnabledAtom,
|
||||
@ -56,7 +54,7 @@ type GPU = {
|
||||
* Advanced Settings Screen
|
||||
* @returns
|
||||
*/
|
||||
const Advanced = () => {
|
||||
const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => {
|
||||
const [experimentalEnabled, setExperimentalEnabled] = useAtom(
|
||||
experimentalFeatureEnabledAtom
|
||||
)
|
||||
@ -64,10 +62,6 @@ const Advanced = () => {
|
||||
const [proxyEnabled, setProxyEnabled] = useAtom(proxyEnabledAtom)
|
||||
const quickAskEnabled = useAtomValue(quickAskEnabledAtom)
|
||||
|
||||
const [proxy, setProxy] = useAtom(proxyAtom)
|
||||
const [ignoreSSL, setIgnoreSSL] = useAtom(ignoreSslAtom)
|
||||
|
||||
const [partialProxy, setPartialProxy] = useState<string>(proxy)
|
||||
const [gpuEnabled, setGpuEnabled] = useState<boolean>(false)
|
||||
const [gpuList, setGpuList] = useState<GPU[]>([])
|
||||
const [gpusInUse, setGpusInUse] = useState<string[]>([])
|
||||
@ -98,22 +92,6 @@ const Advanced = () => {
|
||||
() => configurePullOptions(),
|
||||
300
|
||||
)
|
||||
/**
|
||||
* Handle proxy change
|
||||
*/
|
||||
const onProxyChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value || ''
|
||||
setPartialProxy(value)
|
||||
if (value.trim().startsWith('http')) {
|
||||
setProxy(value.trim())
|
||||
} else {
|
||||
setProxy('')
|
||||
}
|
||||
updatePullOptions()
|
||||
},
|
||||
[setPartialProxy, setProxy, updatePullOptions]
|
||||
)
|
||||
|
||||
/**
|
||||
* Update Quick Ask Enabled
|
||||
@ -448,61 +426,32 @@ const Advanced = () => {
|
||||
|
||||
<DataFolder />
|
||||
|
||||
{/* Proxy */}
|
||||
{/* Proxy Settings Link */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="w-full space-y-1">
|
||||
<div className="flex w-full justify-between gap-x-2">
|
||||
<h6 className="font-semibold capitalize">HTTPS Proxy</h6>
|
||||
<div className="flex w-full cursor-pointer items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">HTTPS Proxy</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Optional proxy server for internet connections
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Optional proxy server for internet connections. Only HTTPS proxies
|
||||
supported.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-shrink-0 flex-col items-end gap-2 pr-1 sm:w-1/2">
|
||||
<Switch
|
||||
data-testid="proxy-switch"
|
||||
checked={proxyEnabled}
|
||||
onChange={() => {
|
||||
setProxyEnabled(!proxyEnabled)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<Input
|
||||
data-testid="proxy-input"
|
||||
placeholder={'http://<user>:<password>@<domain or IP>:<port>'}
|
||||
value={partialProxy}
|
||||
onChange={onProxyChange}
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
data-testid="proxy-switch"
|
||||
checked={proxyEnabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setProxyEnabled(!proxyEnabled)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
<ArrowRightIcon size={16} onClick={() => setSubdir('proxy')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ignore SSL certificates */}
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="flex-shrink-0 space-y-1">
|
||||
<div className="flex gap-x-2">
|
||||
<h6 className="font-semibold capitalize">
|
||||
Ignore SSL certificates
|
||||
</h6>
|
||||
</div>
|
||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
||||
Allow self-signed or unverified certificates - may be required for
|
||||
certain proxies.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="ignore-ssl-switch"
|
||||
checked={ignoreSSL}
|
||||
onChange={(e) => {
|
||||
setIgnoreSSL(e.target.checked)
|
||||
updatePullOptions()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{experimentalEnabled && (
|
||||
<div className="flex w-full flex-col items-start justify-between gap-4 border-b border-[hsla(var(--app-border))] py-4 first:pt-0 last:border-none sm:flex-row">
|
||||
<div className="flex-shrink-0 space-y-1">
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { InferenceEngine } from '@janhq/core'
|
||||
import { useAtomValue } from 'jotai'
|
||||
|
||||
import { useGetEngines } from '@/hooks/useEngineManagement'
|
||||
|
||||
import Advanced from '@/screens/Settings/Advanced'
|
||||
import ProxySettings from '@/screens/Settings/Advanced/ProxySettings'
|
||||
import AppearanceOptions from '@/screens/Settings/Appearance'
|
||||
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
|
||||
import Engines from '@/screens/Settings/Engines'
|
||||
@ -21,6 +24,7 @@ import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
|
||||
const SettingDetail = () => {
|
||||
const selectedSetting = useAtomValue(selectedSettingAtom)
|
||||
const { engines } = useGetEngines()
|
||||
const [subdir, setSubdir] = useState<string | null>(null)
|
||||
|
||||
switch (selectedSetting) {
|
||||
case 'Engines':
|
||||
@ -39,7 +43,12 @@ const SettingDetail = () => {
|
||||
return <Privacy />
|
||||
|
||||
case 'Advanced Settings':
|
||||
return <Advanced />
|
||||
switch (subdir) {
|
||||
case 'proxy':
|
||||
return <ProxySettings onBack={() => setSubdir(null)} />
|
||||
default:
|
||||
return <Advanced setSubdir={setSubdir} />
|
||||
}
|
||||
|
||||
case 'My Models':
|
||||
return <MyModels />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user