From 83550cd0d1bf3d982bd5161c127638f4ad5fed9b Mon Sep 17 00:00:00 2001 From: Doan Bui Date: Fri, 24 Jan 2025 09:31:26 +0700 Subject: [PATCH] Feat: Allow HTTP proxy authentication inputs (#4479) * sub dir * setting proxy * test useConfigurations * fix lint * test * test 2 * update check --- web/helpers/atoms/AppConfig.atom.ts | 54 +++ web/hooks/useConfigurations.test.ts | 137 ++++++ web/hooks/useConfigurations.ts | 45 +- .../Advanced/ProxySettings/index.test.tsx | 147 +++++++ .../Settings/Advanced/ProxySettings/index.tsx | 394 ++++++++++++++++++ web/screens/Settings/Advanced/index.test.tsx | 23 - web/screens/Settings/Advanced/index.tsx | 95 +---- web/screens/Settings/SettingDetail/index.tsx | 11 +- 8 files changed, 806 insertions(+), 100 deletions(-) create mode 100644 web/hooks/useConfigurations.test.ts create mode 100644 web/screens/Settings/Advanced/ProxySettings/index.test.tsx create mode 100644 web/screens/Settings/Advanced/ProxySettings/index.tsx diff --git a/web/helpers/atoms/AppConfig.atom.ts b/web/helpers/atoms/AppConfig.atom.ts index 68a375f3b..db4157422 100644 --- a/web/helpers/atoms/AppConfig.atom.ts +++ b/web/helpers/atoms/AppConfig.atom.ts @@ -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, diff --git a/web/hooks/useConfigurations.test.ts b/web/hooks/useConfigurations.test.ts new file mode 100644 index 000000000..1fab89f59 --- /dev/null +++ b/web/hooks/useConfigurations.test.ts @@ -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: '', + }) + }) +}) \ No newline at end of file diff --git a/web/hooks/useConfigurations.ts b/web/hooks/useConfigurations.ts index 681239033..0bd96e760 100644 --- a/web/hooks/useConfigurations.ts +++ b/web/hooks/useConfigurations.ts @@ -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, diff --git a/web/screens/Settings/Advanced/ProxySettings/index.test.tsx b/web/screens/Settings/Advanced/ProxySettings/index.test.tsx new file mode 100644 index 000000000..1cd4d645f --- /dev/null +++ b/web/screens/Settings/Advanced/ProxySettings/index.test.tsx @@ -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() + + 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() + + const backButton = screen.getByText('Advanced Settings') + fireEvent.click(backButton) + + expect(mockOnBack).toHaveBeenCalled() + }) + + it('toggles password visibility', () => { + render() + + 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() + + // 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() + + 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() + }) + }) +}) \ No newline at end of file diff --git a/web/screens/Settings/Advanced/ProxySettings/index.tsx b/web/screens/Settings/Advanced/ProxySettings/index.tsx new file mode 100644 index 000000000..879aef7f4 --- /dev/null +++ b/web/screens/Settings/Advanced/ProxySettings/index.tsx @@ -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(proxy) + const [proxyUsername, setProxyUsername] = useAtom(proxyUsernameAtom) + const [proxyPassword, setProxyPassword] = useAtom(proxyPasswordAtom) + const [proxyPartialPassword, setProxyPartialPassword] = + useState(proxyPassword) + const [proxyPartialUsername, setProxyPartialUsername] = + useState(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) => { + const value = e.target.value || '' + setPartialProxy(value) + onProxyChange(value) + }, + [setPartialProxy, onProxyChange] + ) + + const handleProxyUsernameInputChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value || '' + setProxyPartialUsername(value) + onProxyUsernameChange(value) + }, + [setProxyPartialUsername, onProxyUsernameChange] + ) + + const handleProxyPasswordInputChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value || '' + setProxyPartialPassword(value) + onProxyPasswordChange(value) + }, + [setProxyPartialPassword, onProxyPasswordChange] + ) + + const onNoProxyChange = useCallback( + (e: React.ChangeEvent) => { + const listNoProxy = e.target.value || '' + const listNoProxyTrim = listNoProxy.split(',').map((item) => item.trim()) + setNoProxy(listNoProxyTrim.join(',')) + updatePullOptions() + }, + [setNoProxy, updatePullOptions] + ) + + return ( + + {/* Header */} +
+
+ + / + + HTTPS Proxy + +
+
+ + {/* Content */} +
+
+
+

Proxy Configuration

+
+ +
+
+
+
+ +

+ URL and port of your proxy server. +

+
+ +
+
+ + {partialProxy && ( + + )} +
+ } + /> +
+
+
+
+
+ +
+
+
+
+ +

+ Credentials for your proxy server (if required). +

+
+
+ + {proxyUsername && ( + + )} +
+ } + /> + + {proxyPassword && ( + + )} + +
+ } + /> +
+
+
+ + + {/* No Proxy */} +
+
+
+
+ +

+ List of hosts that should bypass the proxy. +

+
+
+ + {noProxy && ( + + )} +
+ } + /> +
+
+
+ + +
+

SSL Verification

+
+ + {/* Ignore SSL certificates */} +
+
+
+
+ Ignore SSL certificates +
+
+

+ 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. +

+
+ { + setIgnoreSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Proxy SSL */} +
+
+
+
Verify Proxy SSL
+
+

+ Validate SSL certificate when connecting to the proxy server. +

+
+ { + setVerifyProxySSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Proxy Host SSL */} +
+
+
+
+ Verify Proxy Host SSL +
+
+

+ Validate SSL certificate of the proxy server host. +

+
+ { + setVerifyProxyHostSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Peer SSL */} +
+
+
+
Verify Peer SSL
+
+

+ Validate SSL certificate of the peer connections. +

+
+ { + setVerifyPeerSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + {/* Verify Host SSL */} +
+
+
+
Verify Host SSL
+
+

+ Validate SSL certificate of destination hosts. +

+
+ { + setVerifyHostSSL(e.target.checked) + updatePullOptions() + }} + /> +
+ + +
+ ) +} + +export default ProxySettings diff --git a/web/screens/Settings/Advanced/index.test.tsx b/web/screens/Settings/Advanced/index.test.tsx index 7a0f3ade5..b43b57beb 100644 --- a/web/screens/Settings/Advanced/index.test.tsx +++ b/web/screens/Settings/Advanced/index.test.tsx @@ -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() - 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() - 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() diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 16f1fd0b6..df6db2d2b 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -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(proxy) const [gpuEnabled, setGpuEnabled] = useState(false) const [gpuList, setGpuList] = useState([]) const [gpusInUse, setGpusInUse] = useState([]) @@ -98,22 +92,6 @@ const Advanced = () => { () => configurePullOptions(), 300 ) - /** - * Handle proxy change - */ - const onProxyChange = useCallback( - (event: ChangeEvent) => { - 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 = () => { - {/* Proxy */} + {/* Proxy Settings Link */}
-
-
-
HTTPS Proxy
+
+
+
+
HTTPS Proxy
+
+

+ Optional proxy server for internet connections +

-

- Optional proxy server for internet connections. Only HTTPS proxies - supported. -

-
- -
- { - setProxyEnabled(!proxyEnabled) - updatePullOptions() - }} - /> -
- :@:'} - value={partialProxy} - onChange={onProxyChange} +
+ { + e.stopPropagation() + setProxyEnabled(!proxyEnabled) + updatePullOptions() + }} /> + setSubdir('proxy')} />
- {/* Ignore SSL certificates */} -
-
-
-
- Ignore SSL certificates -
-
-

- Allow self-signed or unverified certificates - may be required for - certain proxies. -

-
- { - setIgnoreSSL(e.target.checked) - updatePullOptions() - }} - /> -
- {experimentalEnabled && (
diff --git a/web/screens/Settings/SettingDetail/index.tsx b/web/screens/Settings/SettingDetail/index.tsx index d4a2c4d82..0d85ccbf4 100644 --- a/web/screens/Settings/SettingDetail/index.tsx +++ b/web/screens/Settings/SettingDetail/index.tsx @@ -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(null) switch (selectedSetting) { case 'Engines': @@ -39,7 +43,12 @@ const SettingDetail = () => { return case 'Advanced Settings': - return + switch (subdir) { + case 'proxy': + return setSubdir(null)} /> + default: + return + } case 'My Models': return