diff options
| author | Oskar <oskar@mullvad.net> | 2024-10-22 14:27:41 +0200 |
|---|---|---|
| committer | Oskar <oskar@mullvad.net> | 2024-10-22 14:27:41 +0200 |
| commit | 8a9c15f4f098c8197c46223bd9a67c734ab020d3 (patch) | |
| tree | ce80a8e9cec4c509beeb5e84cce36c3c015e57a7 | |
| parent | d339daefa6784a1e734ab20bb4a7db9fc8482ec5 (diff) | |
| parent | c63d501223bac2a430a68d824dd37a3f611f4310 (diff) | |
| download | mullvadvpn-8a9c15f4f098c8197c46223bd9a67c734ab020d3.tar.xz mullvadvpn-8a9c15f4f098c8197c46223bd9a67c734ab020d3.zip | |
Merge branch 'implement-ui-for-smart-routing-override-multihop-des-1302'
| -rw-r--r-- | gui/locales/messages.pot | 16 | ||||
| -rw-r--r-- | gui/src/main/default-settings.ts | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/select-location/SelectLocation.tsx | 175 | ||||
| -rw-r--r-- | gui/src/renderer/components/select-location/SelectLocationStyles.tsx | 22 | ||||
| -rw-r--r-- | gui/test/e2e/mocked/select-location.spec.ts | 148 |
5 files changed, 272 insertions, 93 deletions
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 58f3fcc5e9..3850b7f239 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -1345,6 +1345,10 @@ msgctxt "select-location-view" msgid "(Added)" msgstr "" +msgctxt "select-location-view" +msgid "%(daita)s overrides %(multihop)s. To use %(multihop)s, please enable “%(directOnly)s” or disable %(daita)s in the %(daita)s settings." +msgstr "" + #. This is used for appending information about a location. #. E.g. "Gothenburg (Entry)" if Gothenburg has been selected as the entrypoint. #. Available placeholders: @@ -1406,6 +1410,10 @@ msgid "Filtered:" msgstr "" msgctxt "select-location-view" +msgid "Go to %(daita)s settings" +msgstr "" + +msgctxt "select-location-view" msgid "List names must be unique." msgstr "" @@ -1429,14 +1437,6 @@ msgctxt "select-location-view" msgid "The app selects a random bridge server, but servers have a higher probability the closer they are to you." msgstr "" -msgctxt "select-location-view" -msgid "While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server)." -msgstr "" - -msgctxt "select-location-view" -msgid "While connected, your traffic will be routed through two secure locations, the entry point and the exit point (needs to be two different VPN servers)." -msgstr "" - msgctxt "settings-import" msgid "Clear all overrides" msgstr "" diff --git a/gui/src/main/default-settings.ts b/gui/src/main/default-settings.ts index e11a7434e1..46355dd439 100644 --- a/gui/src/main/default-settings.ts +++ b/gui/src/main/default-settings.ts @@ -53,6 +53,10 @@ export function getDefaultSettings(): ISettings { wireguard: { mtu: undefined, quantumResistant: undefined, + daita: { + enabled: false, + directOnly: false, + }, }, dns: { state: 'default', diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx index 01297f5630..438fdc430d 100644 --- a/gui/src/renderer/components/select-location/SelectLocation.tsx +++ b/gui/src/renderer/components/select-location/SelectLocation.tsx @@ -1,7 +1,7 @@ import { useCallback, useState } from 'react'; import { sprintf } from 'sprintf-js'; -import { colors } from '../../../config.json'; +import { colors, strings } from '../../../config.json'; import { Ownership } from '../../../shared/daemon-rpc-types'; import { messages } from '../../../shared/gettext'; import { useRelaySettingsUpdater } from '../../lib/constraint-updater'; @@ -39,14 +39,14 @@ import { useSelectLocationContext } from './SelectLocationContainer'; import { StyledClearFilterButton, StyledContent, + StyledDaitaSettingsButton, StyledFilter, StyledFilterRow, - StyledHeaderSubTitle, StyledNavigationBarAttachment, - StyledNoResult, - StyledNoResultText, StyledScopeBar, StyledSearchBar, + StyledSelectionUnavailable, + StyledSelectionUnavailableText, } from './SelectLocationStyles'; import { SpacePreAllocationView } from './SpacePreAllocationView'; import { @@ -163,81 +163,69 @@ export default function SelectLocation() { </ScopeBarItem> <ScopeBarItem>{messages.pgettext('select-location-view', 'Exit')}</ScopeBarItem> </StyledScopeBar> - - {tunnelProtocol === 'openvpn' ? ( - <StyledHeaderSubTitle> - {messages.pgettext( - 'select-location-view', - 'While connected, your traffic will be routed through two secure locations, the entry point (a bridge server) and the exit point (a VPN server).', - )} - </StyledHeaderSubTitle> - ) : ( - <StyledHeaderSubTitle> - {messages.pgettext( - 'select-location-view', - 'While connected, your traffic will be routed through two secure locations, the entry point and the exit point (needs to be two different VPN servers).', - )} - </StyledHeaderSubTitle> - )} </> )} - {showFilters && ( - <StyledFilterRow> - {messages.pgettext('select-location-view', 'Filtered:')} + {locationType === LocationType.entry && daita && !directOnly ? null : ( + <> + {showFilters && ( + <StyledFilterRow> + {messages.pgettext('select-location-view', 'Filtered:')} - {showOwnershipFilter && ( - <StyledFilter> - {ownershipFilterLabel(ownership)} - <StyledClearFilterButton - aria-label={messages.gettext('Clear')} - onClick={onClearOwnership}> - <ImageView - height={16} - width={16} - source="icon-close" - tintColor={colors.white60} - tintHoverColor={colors.white80} - /> - </StyledClearFilterButton> - </StyledFilter> - )} + {showOwnershipFilter && ( + <StyledFilter> + {ownershipFilterLabel(ownership)} + <StyledClearFilterButton + aria-label={messages.gettext('Clear')} + onClick={onClearOwnership}> + <ImageView + height={16} + width={16} + source="icon-close" + tintColor={colors.white60} + tintHoverColor={colors.white80} + /> + </StyledClearFilterButton> + </StyledFilter> + )} - {showProvidersFilter && ( - <StyledFilter> - {sprintf( - messages.pgettext( - 'select-location-view', - 'Providers: %(numberOfProviders)d', - ), - { numberOfProviders: filteredProviders.length }, + {showProvidersFilter && ( + <StyledFilter> + {sprintf( + messages.pgettext( + 'select-location-view', + 'Providers: %(numberOfProviders)d', + ), + { numberOfProviders: filteredProviders.length }, + )} + <StyledClearFilterButton + aria-label={messages.gettext('Clear')} + onClick={onClearProviders}> + <ImageView + height={16} + width={16} + source="icon-close" + tintColor={colors.white60} + tintHoverColor={colors.white80} + /> + </StyledClearFilterButton> + </StyledFilter> )} - <StyledClearFilterButton - aria-label={messages.gettext('Clear')} - onClick={onClearProviders}> - <ImageView - height={16} - width={16} - source="icon-close" - tintColor={colors.white60} - tintHoverColor={colors.white80} - /> - </StyledClearFilterButton> - </StyledFilter> - )} - {showDaitaFilter && ( - <StyledFilter> - {sprintf( - messages.pgettext('select-location-view', 'Setting: %(settingName)s'), - { settingName: 'DAITA' }, + {showDaitaFilter && ( + <StyledFilter> + {sprintf( + messages.pgettext('select-location-view', 'Setting: %(settingName)s'), + { settingName: 'DAITA' }, + )} + </StyledFilter> )} - </StyledFilter> + </StyledFilterRow> )} - </StyledFilterRow> - )} - <StyledSearchBar searchTerm={searchValue} onSearch={updateSearchTerm} /> + <StyledSearchBar searchTerm={searchValue} onSearch={updateSearchTerm} /> + </> + )} </StyledNavigationBarAttachment> <NavigationScrollbars ref={scrollViewRef}> @@ -273,6 +261,9 @@ function SelectLocationContent() { const [onSelectEntryRelay, onSelectEntrySpecial] = useOnSelectEntryLocation(); const [onSelectBridgeRelay, onSelectBridgeSpecial] = useOnSelectBridgeLocation(); + const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); + const directOnly = useSelector((state) => state.settings.wireguard.daita?.directOnly ?? false); + const relaySettings = useNormalRelaySettings(); const bridgeSettings = useSelector((state) => state.settings.bridgeSettings); @@ -311,6 +302,10 @@ function SelectLocationContent() { </> ); } else if (relaySettings?.tunnelProtocol !== 'openvpn') { + if (daita && !directOnly) { + return <DisabledEntrySelection />; + } + return ( <> <CustomLists selectedElementRef={selectedLocationRef} onSelect={onSelectEntryRelay} /> @@ -409,15 +404,47 @@ function NoSearchResult(props: NoSearchResultProps) { } return ( - <StyledNoResult> - <StyledNoResultText> + <StyledSelectionUnavailable> + <StyledSelectionUnavailableText> {formatHtml( sprintf(messages.gettext('No result for <b>%(searchTerm)s</b>.'), { searchTerm, }), )} - </StyledNoResultText> - <StyledNoResultText>{messages.gettext('Try a different search.')}</StyledNoResultText> - </StyledNoResult> + </StyledSelectionUnavailableText> + <StyledSelectionUnavailableText> + {messages.gettext('Try a different search.')} + </StyledSelectionUnavailableText> + </StyledSelectionUnavailable> + ); +} + +function DisabledEntrySelection() { + const { push } = useHistory(); + + const multihop = messages.pgettext('settings-view', 'Multihop'); + const directOnly = messages.gettext('Direct only'); + + const navigateToDaitaSettings = useCallback(() => { + push(RoutePath.daitaSettings); + }, [push]); + + return ( + <StyledSelectionUnavailable> + <StyledSelectionUnavailableText> + {sprintf( + messages.pgettext( + 'select-location-view', + '%(daita)s overrides %(multihop)s. To use %(multihop)s, please enable “%(directOnly)s” or disable %(daita)s in the %(daita)s settings.', + ), + { daita: strings.daita, multihop, directOnly }, + )} + </StyledSelectionUnavailableText> + <StyledDaitaSettingsButton onClick={navigateToDaitaSettings}> + {sprintf(messages.pgettext('select-location-view', 'Go to %(daita)s settings'), { + daita: strings.daita, + })} + </StyledDaitaSettingsButton> + </StyledSelectionUnavailable> ); } diff --git a/gui/src/renderer/components/select-location/SelectLocationStyles.tsx b/gui/src/renderer/components/select-location/SelectLocationStyles.tsx index fd401c8e8c..ff83a6fc8d 100644 --- a/gui/src/renderer/components/select-location/SelectLocationStyles.tsx +++ b/gui/src/renderer/components/select-location/SelectLocationStyles.tsx @@ -4,7 +4,7 @@ import { colors } from '../../../config.json'; import * as Cell from '../cell'; import { normalText, tinyText } from '../common-styles'; import SearchBar from '../SearchBar'; -import { HeaderSubTitle } from '../SettingsHeader'; +import { SmallButton } from '../SmallButton'; import { ScopeBar } from './ScopeBar'; export const StyledContent = styled.div({ @@ -15,17 +15,17 @@ export const StyledContent = styled.div({ }); export const StyledScopeBar = styled(ScopeBar)({ - marginBottom: '14px', + marginBottom: '16px', }); export const StyledNavigationBarAttachment = styled.div({ - padding: '0 16px 14px', + padding: '0 16px 16px', }); export const StyledFilterRow = styled.div({ ...tinyText, color: colors.white, - margin: '0 6px 14px', + margin: '0 6px 16px', }); export const StyledFilter = styled.div({ @@ -48,26 +48,26 @@ export const StyledClearFilterButton = styled.div({ backgroundColor: 'transparent', }); -export const StyledHeaderSubTitle = styled(HeaderSubTitle)({ - display: 'block', - margin: '0 6px 14px', -}); - export const StyledSearchBar = styled(SearchBar)({ margin: '0 6px', }); -export const StyledNoResult = styled(Cell.CellFooter)({ +export const StyledSelectionUnavailable = styled(Cell.CellFooter)({ display: 'flex', flexDirection: 'column', paddingTop: 0, marginTop: 0, }); -export const StyledNoResultText = styled(Cell.CellFooterText)({ +export const StyledSelectionUnavailableText = styled(Cell.CellFooterText)({ textAlign: 'center', }); export const StyledAllLocationsTitle = styled(Cell.Label)(normalText, { fontWeight: 'normal', }); + +export const StyledDaitaSettingsButton = styled(SmallButton)({ + marginLeft: 0, + marginTop: '24px', +}); diff --git a/gui/test/e2e/mocked/select-location.spec.ts b/gui/test/e2e/mocked/select-location.spec.ts new file mode 100644 index 0000000000..342d511b9e --- /dev/null +++ b/gui/test/e2e/mocked/select-location.spec.ts @@ -0,0 +1,148 @@ +import { expect, test } from '@playwright/test'; +import { Page } from 'playwright'; + +import { colors } from '../../../src/config.json'; +import { getDefaultSettings } from '../../../src/main/default-settings'; +import { + IRelayList, + IRelayListWithEndpointData, + ISettings, + IWireguardEndpointData, +} from '../../../src/shared/daemon-rpc-types'; +import { MockedTestUtils, startMockedApp } from './mocked-utils'; + +const relayList: IRelayList = { + countries: [ + { + name: 'Sweden', + code: 'se', + cities: [ + { + name: 'Gothenburg', + code: 'got', + latitude: 58, + longitude: 12, + relays: [ + { + hostname: 'se-got-wg-101', + provider: 'mullvad', + ipv4AddrIn: '10.0.0.1', + includeInCountry: true, + active: true, + weight: 0, + owned: true, + endpointType: 'wireguard', + daita: true, + }, + { + hostname: 'se-got-wg-102', + provider: 'mullvad', + ipv4AddrIn: '10.0.0.2', + includeInCountry: true, + active: true, + weight: 0, + owned: true, + endpointType: 'wireguard', + daita: true, + }, + ], + }, + ], + }, + ], +}; + +const wireguardEndpointData: IWireguardEndpointData = { + portRanges: [], + udp2tcpPorts: [], +}; + +let page: Page; +let util: MockedTestUtils; + +test.beforeAll(async () => { + ({ page, util } = await startMockedApp()); + await setMultihop(); + await util.waitForNavigation(() => page.getByLabel('Select location').click()); +}); + +test.afterAll(async () => { + await page.close(); +}); + +async function setMultihop() { + const settings = getDefaultSettings(); + if ('normal' in settings.relaySettings) { + settings.relaySettings.normal.wireguardConstraints.useMultihop = true; + } + + await util.sendMockIpcResponse<ISettings>({ + channel: 'settings-', + response: settings, + }); + + await util.sendMockIpcResponse<IRelayListWithEndpointData>({ + channel: 'relays-', + response: { relayList, wireguardEndpointData }, + }); +} + +test('App should show entry selection', async () => { + const entryTab = page.getByText('Entry'); + await entryTab.click(); + await expect(entryTab).toHaveCSS('background-color', colors.green); + + const sweden = page.getByText('Sweden'); + await expect(sweden).toBeVisible(); +}); + +test('App should show exit selection', async () => { + const exitTab = page.getByText('Exit'); + await exitTab.click(); + await expect(exitTab).toHaveCSS('background-color', colors.green); + + const sweden = page.getByText('Sweden'); + await expect(sweden).toBeVisible(); +}); + +test("App shouldn't show entry selection when daita is enabled without direct only", async () => { + const settings = getDefaultSettings(); + if ('normal' in settings.relaySettings && settings.tunnelOptions.wireguard.daita) { + settings.relaySettings.normal.wireguardConstraints.useMultihop = true; + settings.tunnelOptions.wireguard.daita.enabled = true; + settings.tunnelOptions.wireguard.daita.directOnly = false; + } + + await util.sendMockIpcResponse<ISettings>({ + channel: 'settings-', + response: settings, + }); + + const entryTab = page.getByText('Entry'); + await entryTab.click(); + await expect(entryTab).toHaveCSS('background-color', colors.green); + + const sweden = page.getByText('Sweden'); + await expect(sweden).not.toBeVisible(); +}); + +test('App should show entry selection when daita is enabled with direct only', async () => { + const settings = getDefaultSettings(); + if ('normal' in settings.relaySettings && settings.tunnelOptions.wireguard.daita) { + settings.relaySettings.normal.wireguardConstraints.useMultihop = true; + settings.tunnelOptions.wireguard.daita.enabled = true; + settings.tunnelOptions.wireguard.daita.directOnly = true; + } + + await util.sendMockIpcResponse<ISettings>({ + channel: 'settings-', + response: settings, + }); + + const entryTab = page.getByText('Entry'); + await entryTab.click(); + await expect(entryTab).toHaveCSS('background-color', colors.green); + + const sweden = page.getByText('Sweden'); + await expect(sweden).toBeVisible(); +}); |
