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 PROXY_FEATURE_ENABLED = 'proxyFeatureEnabled'
|
||||||
const VULKAN_ENABLED = 'vulkanEnabled'
|
const VULKAN_ENABLED = 'vulkanEnabled'
|
||||||
const IGNORE_SSL = 'ignoreSSLFeature'
|
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 HTTPS_PROXY_FEATURE = 'httpsProxyFeature'
|
||||||
|
const PROXY_USERNAME = 'proxyUsername'
|
||||||
|
const PROXY_PASSWORD = 'proxyPassword'
|
||||||
const QUICK_ASK_ENABLED = 'quickAskEnabled'
|
const QUICK_ASK_ENABLED = 'quickAskEnabled'
|
||||||
|
const NO_PROXY = 'noProxy'
|
||||||
|
|
||||||
export const janDataFolderPathAtom = atom('')
|
export const janDataFolderPathAtom = atom('')
|
||||||
|
|
||||||
@ -27,9 +34,56 @@ export const proxyAtom = atomWithStorage(HTTPS_PROXY_FEATURE, '', undefined, {
|
|||||||
getOnInit: true,
|
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, {
|
export const ignoreSslAtom = atomWithStorage(IGNORE_SSL, false, undefined, {
|
||||||
getOnInit: true,
|
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(
|
export const vulkanEnabledAtom = atomWithStorage(
|
||||||
VULKAN_ENABLED,
|
VULKAN_ENABLED,
|
||||||
false,
|
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 { extensionManager } from '@/extension'
|
||||||
import {
|
import {
|
||||||
ignoreSslAtom,
|
ignoreSslAtom,
|
||||||
|
noProxyAtom,
|
||||||
proxyAtom,
|
proxyAtom,
|
||||||
proxyEnabledAtom,
|
proxyEnabledAtom,
|
||||||
|
proxyPasswordAtom,
|
||||||
|
proxyUsernameAtom,
|
||||||
|
verifyHostSslAtom,
|
||||||
|
verifyPeerSslAtom,
|
||||||
|
verifyProxyHostSslAtom,
|
||||||
|
verifyProxySslAtom,
|
||||||
} from '@/helpers/atoms/AppConfig.atom'
|
} from '@/helpers/atoms/AppConfig.atom'
|
||||||
|
|
||||||
export const useConfigurations = () => {
|
export const useConfigurations = () => {
|
||||||
const proxyEnabled = useAtomValue(proxyEnabledAtom)
|
const proxyEnabled = useAtomValue(proxyEnabledAtom)
|
||||||
const proxyUrl = useAtomValue(proxyAtom)
|
const proxyUrl = useAtomValue(proxyAtom)
|
||||||
const proxyIgnoreSSL = useAtomValue(ignoreSslAtom)
|
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(() => {
|
const configurePullOptions = useCallback(() => {
|
||||||
extensionManager
|
extensionManager
|
||||||
@ -21,20 +35,45 @@ export const useConfigurations = () => {
|
|||||||
?.configurePullOptions(
|
?.configurePullOptions(
|
||||||
proxyEnabled
|
proxyEnabled
|
||||||
? {
|
? {
|
||||||
|
proxy_username: proxyUsername,
|
||||||
|
proxy_password: proxyPassword,
|
||||||
proxy_url: proxyUrl,
|
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: '',
|
proxy_url: '',
|
||||||
|
verify_proxy_ssl: false,
|
||||||
|
verify_proxy_host_ssl: false,
|
||||||
verify_peer_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(() => {
|
useEffect(() => {
|
||||||
configurePullOptions()
|
configurePullOptions()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [configurePullOptions])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
configurePullOptions,
|
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(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('Experimental Mode')).toBeInTheDocument()
|
expect(screen.getByText('Experimental Mode')).toBeInTheDocument()
|
||||||
expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument()
|
expect(screen.getByText('HTTPS Proxy')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Ignore SSL certificates')).toBeInTheDocument()
|
|
||||||
expect(screen.getByText('Jan Data Folder')).toBeInTheDocument()
|
expect(screen.getByText('Jan Data Folder')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument()
|
expect(screen.getByText('Reset to Factory Settings')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
@ -102,28 +101,6 @@ describe('Advanced', () => {
|
|||||||
expect(proxyToggle).toBeChecked()
|
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 () => {
|
it('renders DataFolder component', async () => {
|
||||||
render(<Advanced />)
|
render(<Advanced />)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, ChangeEvent } from 'react'
|
import { useEffect, useState, ChangeEvent } from 'react'
|
||||||
|
|
||||||
import { openExternalUrl, AppConfiguration } from '@janhq/core'
|
import { openExternalUrl, AppConfiguration } from '@janhq/core'
|
||||||
|
|
||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from '@janhq/joi'
|
} from '@janhq/joi'
|
||||||
|
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
|
||||||
import { ChevronDownIcon } from 'lucide-react'
|
import { ChevronDownIcon, ArrowRightIcon } from 'lucide-react'
|
||||||
import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react'
|
import { AlertTriangleIcon, AlertCircleIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
@ -35,8 +35,6 @@ import FactoryReset from './FactoryReset'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
experimentalFeatureEnabledAtom,
|
experimentalFeatureEnabledAtom,
|
||||||
ignoreSslAtom,
|
|
||||||
proxyAtom,
|
|
||||||
proxyEnabledAtom,
|
proxyEnabledAtom,
|
||||||
vulkanEnabledAtom,
|
vulkanEnabledAtom,
|
||||||
quickAskEnabledAtom,
|
quickAskEnabledAtom,
|
||||||
@ -56,7 +54,7 @@ type GPU = {
|
|||||||
* Advanced Settings Screen
|
* Advanced Settings Screen
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const Advanced = () => {
|
const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => {
|
||||||
const [experimentalEnabled, setExperimentalEnabled] = useAtom(
|
const [experimentalEnabled, setExperimentalEnabled] = useAtom(
|
||||||
experimentalFeatureEnabledAtom
|
experimentalFeatureEnabledAtom
|
||||||
)
|
)
|
||||||
@ -64,10 +62,6 @@ const Advanced = () => {
|
|||||||
const [proxyEnabled, setProxyEnabled] = useAtom(proxyEnabledAtom)
|
const [proxyEnabled, setProxyEnabled] = useAtom(proxyEnabledAtom)
|
||||||
const quickAskEnabled = useAtomValue(quickAskEnabledAtom)
|
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 [gpuEnabled, setGpuEnabled] = useState<boolean>(false)
|
||||||
const [gpuList, setGpuList] = useState<GPU[]>([])
|
const [gpuList, setGpuList] = useState<GPU[]>([])
|
||||||
const [gpusInUse, setGpusInUse] = useState<string[]>([])
|
const [gpusInUse, setGpusInUse] = useState<string[]>([])
|
||||||
@ -98,22 +92,6 @@ const Advanced = () => {
|
|||||||
() => configurePullOptions(),
|
() => configurePullOptions(),
|
||||||
300
|
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
|
* Update Quick Ask Enabled
|
||||||
@ -448,61 +426,32 @@ const Advanced = () => {
|
|||||||
|
|
||||||
<DataFolder />
|
<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="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 cursor-pointer items-center justify-between">
|
||||||
<div className="flex w-full justify-between gap-x-2">
|
<div className="space-y-1">
|
||||||
<h6 className="font-semibold capitalize">HTTPS Proxy</h6>
|
<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>
|
</div>
|
||||||
<p className="font-medium leading-relaxed text-[hsla(var(--text-secondary))]">
|
<div className="flex items-center gap-2">
|
||||||
Optional proxy server for internet connections. Only HTTPS proxies
|
<Switch
|
||||||
supported.
|
data-testid="proxy-switch"
|
||||||
</p>
|
checked={proxyEnabled}
|
||||||
</div>
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
<div className="flex w-full flex-shrink-0 flex-col items-end gap-2 pr-1 sm:w-1/2">
|
setProxyEnabled(!proxyEnabled)
|
||||||
<Switch
|
updatePullOptions()
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
|
<ArrowRightIcon size={16} onClick={() => setSubdir('proxy')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 && (
|
{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 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-shrink-0 space-y-1">
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { InferenceEngine } from '@janhq/core'
|
import { InferenceEngine } from '@janhq/core'
|
||||||
import { useAtomValue } from 'jotai'
|
import { useAtomValue } from 'jotai'
|
||||||
|
|
||||||
import { useGetEngines } from '@/hooks/useEngineManagement'
|
import { useGetEngines } from '@/hooks/useEngineManagement'
|
||||||
|
|
||||||
import Advanced from '@/screens/Settings/Advanced'
|
import Advanced from '@/screens/Settings/Advanced'
|
||||||
|
import ProxySettings from '@/screens/Settings/Advanced/ProxySettings'
|
||||||
import AppearanceOptions from '@/screens/Settings/Appearance'
|
import AppearanceOptions from '@/screens/Settings/Appearance'
|
||||||
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
|
import ExtensionCatalog from '@/screens/Settings/CoreExtensions'
|
||||||
import Engines from '@/screens/Settings/Engines'
|
import Engines from '@/screens/Settings/Engines'
|
||||||
@ -21,6 +24,7 @@ import { selectedSettingAtom } from '@/helpers/atoms/Setting.atom'
|
|||||||
const SettingDetail = () => {
|
const SettingDetail = () => {
|
||||||
const selectedSetting = useAtomValue(selectedSettingAtom)
|
const selectedSetting = useAtomValue(selectedSettingAtom)
|
||||||
const { engines } = useGetEngines()
|
const { engines } = useGetEngines()
|
||||||
|
const [subdir, setSubdir] = useState<string | null>(null)
|
||||||
|
|
||||||
switch (selectedSetting) {
|
switch (selectedSetting) {
|
||||||
case 'Engines':
|
case 'Engines':
|
||||||
@ -39,7 +43,12 @@ const SettingDetail = () => {
|
|||||||
return <Privacy />
|
return <Privacy />
|
||||||
|
|
||||||
case 'Advanced Settings':
|
case 'Advanced Settings':
|
||||||
return <Advanced />
|
switch (subdir) {
|
||||||
|
case 'proxy':
|
||||||
|
return <ProxySettings onBack={() => setSubdir(null)} />
|
||||||
|
default:
|
||||||
|
return <Advanced setSubdir={setSubdir} />
|
||||||
|
}
|
||||||
|
|
||||||
case 'My Models':
|
case 'My Models':
|
||||||
return <MyModels />
|
return <MyModels />
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user