diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-08-14 12:41:50 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-08-14 12:41:50 +0200 |
| commit | d94f70ec81d291b5d8d348c40058e25e81e07562 (patch) | |
| tree | d5bf20348b6c7e502ba4d735f939ff2f618c1d10 /gui/src/renderer | |
| parent | faf933331f93e1a8d771babb31f3ac37a2f9851f (diff) | |
| parent | 9ecbf3a72e2d4f8a4fb2ce6794beb49cda03bf34 (diff) | |
| download | mullvadvpn-d94f70ec81d291b5d8d348c40058e25e81e07562.tar.xz mullvadvpn-d94f70ec81d291b5d8d348c40058e25e81e07562.zip | |
Merge branch 'linux-split-tunneling-gui' into master
Diffstat (limited to 'gui/src/renderer')
| -rw-r--r-- | gui/src/renderer/app.tsx | 9 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettings.tsx | 21 | ||||
| -rw-r--r-- | gui/src/renderer/components/AdvancedSettingsStyles.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/ImageView.tsx | 5 | ||||
| -rw-r--r-- | gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx | 279 | ||||
| -rw-r--r-- | gui/src/renderer/components/Modal.tsx | 5 | ||||
| -rw-r--r-- | gui/src/renderer/containers/AdvancedSettingsPage.tsx | 1 | ||||
| -rw-r--r-- | gui/src/renderer/routes.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/transitions.ts | 1 |
9 files changed, 325 insertions, 6 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index c09ca39049..cc65289a26 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -26,6 +26,7 @@ import { ICurrentAppVersionInfo } from '../main'; import { loadTranslations, messages, relayLocations } from '../shared/gettext'; import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state'; import { IpcRendererEventChannel, IRelayListPair } from '../shared/ipc-event-channel'; +import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application'; import { getRendererLogFile, setupLogging } from '../shared/logging'; import consumePromise from '../shared/promise'; @@ -413,6 +414,14 @@ export default class AppRenderer { actions.settings.setWireguardKeygenEvent(keygenEvent); } + public getSplitTunnelingApplications() { + return IpcRendererEventChannel.splitTunneling.getApplications(); + } + + public launchExcludedApplication(application: ISplitTunnelingApplication | string) { + consumePromise(IpcRendererEventChannel.splitTunneling.launchApplication(application)); + } + public getPreferredLocaleList(): IPreferredLocaleDescriptor[] { return [ { diff --git a/gui/src/renderer/components/AdvancedSettings.tsx b/gui/src/renderer/components/AdvancedSettings.tsx index 21ab4ae88d..7d6724b001 100644 --- a/gui/src/renderer/components/AdvancedSettings.tsx +++ b/gui/src/renderer/components/AdvancedSettings.tsx @@ -62,6 +62,7 @@ interface IProps { setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void; setWireguardRelayPort: (port?: number) => void; onViewWireguardKeys: () => void; + onViewLinuxSplitTunneling: () => void; onClose: () => void; } @@ -204,7 +205,7 @@ export default class AdvancedSettings extends Component<IProps, IState> { <View style={[ styles.advanced_settings__content, - styles.advanced_settings__tunnel_protocol, + styles.advanced_settings__cell_bottom_margin, ]}> <TunnelProtocolSelector title={messages.pgettext('advanced-settings-view', 'Tunnel protocol')} @@ -350,7 +351,12 @@ export default class AdvancedSettings extends Component<IProps, IState> { </Cell.FooterText> </Cell.Footer> - <View style={styles.advanced_settings__wgkeys_cell}> + <View + style={ + process.platform !== 'linux' + ? styles.advanced_settings__last_cell_bottom_margin + : undefined + }> <Cell.CellButton onClick={this.props.onViewWireguardKeys}> <Cell.Label> {messages.pgettext('advanced-settings-view', 'WireGuard key')} @@ -358,6 +364,17 @@ export default class AdvancedSettings extends Component<IProps, IState> { <Cell.Icon height={12} width={7} source="icon-chevron" /> </Cell.CellButton> </View> + + {process.platform === 'linux' && ( + <View style={styles.advanced_settings__last_cell_bottom_margin}> + <Cell.CellButton onClick={this.props.onViewLinuxSplitTunneling}> + <Cell.Label> + {messages.pgettext('advanced-settings-view', 'Split tunneling')} + </Cell.Label> + <Cell.Icon height={12} width={7} source="icon-chevron" /> + </Cell.CellButton> + </View> + )} </StyledNavigationScrollbars> </View> </NavigationContainer> diff --git a/gui/src/renderer/components/AdvancedSettingsStyles.tsx b/gui/src/renderer/components/AdvancedSettingsStyles.tsx index 421cb86a64..e39a7b4ed7 100644 --- a/gui/src/renderer/components/AdvancedSettingsStyles.tsx +++ b/gui/src/renderer/components/AdvancedSettingsStyles.tsx @@ -28,10 +28,10 @@ export default { advanced_settings__content: Styles.createViewStyle({ flex: 0, }), - advanced_settings__tunnel_protocol: Styles.createViewStyle({ + advanced_settings__cell_bottom_margin: Styles.createViewStyle({ marginBottom: 20, }), - advanced_settings__wgkeys_cell: Styles.createViewStyle({ + advanced_settings__last_cell_bottom_margin: Styles.createViewStyle({ marginBottom: 22, }), advanced_settings__wg_no_key: Styles.createTextStyle({ diff --git a/gui/src/renderer/components/ImageView.tsx b/gui/src/renderer/components/ImageView.tsx index 3b7ea88e84..6765b89a53 100644 --- a/gui/src/renderer/components/ImageView.tsx +++ b/gui/src/renderer/components/ImageView.tsx @@ -1,3 +1,4 @@ +import path from 'path'; import * as React from 'react'; import styled from 'styled-components'; @@ -40,7 +41,9 @@ const ImageMask = styled.div((props: IImageMaskProps) => { const HiddenImage = styled.img({ visibility: 'hidden' }); export default function ImageView(props: IImageViewProps) { - const url = `../../assets/images/${props.source}.svg`; + const url = path.isAbsolute(props.source) + ? props.source + : `../../assets/images/${props.source}.svg`; if (props.tintColor) { const { source: _source, ...otherProps } = props; diff --git a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx new file mode 100644 index 0000000000..b018221932 --- /dev/null +++ b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx @@ -0,0 +1,279 @@ +import { remote } from 'electron'; +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { useHistory } from 'react-router'; +import { sprintf } from 'sprintf-js'; +import styled from 'styled-components'; +import { colors } from '../../config.json'; +import { messages } from '../../shared/gettext'; +import ISplitTunnelingApplication from '../../shared/linux-split-tunneling-application'; +import consumePromise from '../../shared/promise'; +import { useAppContext } from '../context'; +import * as AppButton from './AppButton'; +import * as Cell from './Cell'; +import ImageView from './ImageView'; +import { Container, Layout } from './Layout'; +import { ModalContainer, ModalAlert, ModalAlertType } from './Modal'; +import { + BackBarItem, + NavigationBar, + NavigationContainer, + NavigationItems, + NavigationScrollbars, + TitleBarItem, +} from './NavigationBar'; +import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; + +const StyledPageCover = styled.div({}, (props: { show: boolean }) => ({ + position: 'absolute', + zIndex: 2, + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: '#000000', + opacity: 0.6, + display: props.show ? 'block' : 'none', +})); + +const StyledContainer = styled(Container)({ + backgroundColor: colors.darkBlue, +}); + +const StyledNavigationScrollbars = styled(NavigationScrollbars)({ + flex: 1, +}); + +const StyledContent = styled.div({ + display: 'flex', + flexDirection: 'column', + flex: 1, +}); + +const StyledCellButton = styled(Cell.CellButton)((props: { lookDisabled: boolean }) => ({ + ':not(:disabled):hover': { + backgroundColor: props.lookDisabled ? colors.blue : undefined, + }, +})); + +const disabledApplication = (props: { lookDisabled: boolean }) => ({ + opacity: props.lookDisabled ? 0.6 : undefined, +}); + +const StyledIcon = styled(Cell.UntintedIcon)(disabledApplication, { + marginRight: '12px', +}); + +const StyledCellLabel = styled(Cell.Label)(disabledApplication, { + fontWeight: 'normal', +}); + +const StyledIconPlaceholder = styled.div({ + width: '35px', + marginRight: '12px', +}); + +const StyledApplicationListContent = styled.div({ + display: 'flex', + flexDirection: 'column', +}); + +const StyledApplicationListAnimation = styled.div({}, (props: { height?: number }) => ({ + overflow: 'hidden', + height: props.height ? `${props.height}px` : 'auto', + transition: 'height 500ms ease-in-out', + marginBottom: '20px', +})); + +const StyledSpinnerRow = styled.div({ + display: 'flex', + justifyContent: 'center', + padding: '8px 0', + background: colors.blue40, +}); + +const StyledBrowseButton = styled(AppButton.BlueButton)({ + margin: '0 22px 22px', +}); + +export default function LinuxSplitTunnelingSettings() { + const { getSplitTunnelingApplications, launchExcludedApplication } = useAppContext(); + const history = useHistory(); + + const [applications, setApplications] = useState<ISplitTunnelingApplication[]>(); + const [applicationListHeight, setApplicationListHeight] = useState<number>(); + const [browsing, setBrowsing] = useState(false); + + const applicationListRef = useRef() as React.RefObject<HTMLDivElement>; + + const launchWithFilePicker = useCallback(async () => { + setBrowsing(true); + const file = await remote.dialog.showOpenDialog({ + properties: ['openFile'], + buttonLabel: messages.pgettext('split-tunneling-view', 'Launch application'), + }); + setBrowsing(false); + + if (file.filePaths[0]) { + launchExcludedApplication(file.filePaths[0]); + } + }, []); + + useEffect(() => { + consumePromise(getSplitTunnelingApplications().then(setApplications)); + }, []); + + useLayoutEffect(() => { + const height = applicationListRef.current?.getBoundingClientRect().height; + setApplicationListHeight(height); + }, [applications]); + + return ( + <> + <StyledPageCover show={browsing} /> + <ModalContainer> + <Layout> + <StyledContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <BackBarItem action={history.goBack}> + { + // TRANSLATORS: Back button in navigation bar + messages.pgettext('navigation-bar', 'Advanced') + } + </BackBarItem> + <TitleBarItem> + { + // TRANSLATORS: Title label in navigation bar + messages.pgettext('split-tunneling-nav', 'Split tunneling') + } + </TitleBarItem> + </NavigationItems> + </NavigationBar> + + <StyledNavigationScrollbars> + <StyledContent> + <SettingsHeader> + <HeaderTitle> + {messages.pgettext('split-tunneling-view', 'Split tunneling')} + </HeaderTitle> + <HeaderSubTitle> + {messages.pgettext( + 'split-tunneling-view', + 'Click on an app to launch it. Its traffic will bypass the VPN tunnel until you close it.', + )} + </HeaderSubTitle> + </SettingsHeader> + + <StyledApplicationListAnimation height={applicationListHeight}> + <StyledApplicationListContent ref={applicationListRef}> + {applications === undefined ? ( + <StyledSpinnerRow> + <ImageView source="icon-spinner" height={60} width={60} /> + </StyledSpinnerRow> + ) : ( + applications.map((application) => ( + <ApplicationRow + key={application.absolutepath} + application={application} + launchApplication={launchExcludedApplication} + /> + )) + )} + </StyledApplicationListContent> + </StyledApplicationListAnimation> + + <StyledBrowseButton onClick={launchWithFilePicker}> + {messages.pgettext('split-tunneling-view', 'Browse')} + </StyledBrowseButton> + </StyledContent> + </StyledNavigationScrollbars> + </NavigationContainer> + </StyledContainer> + </Layout> + </ModalContainer> + </> + ); +} + +interface IApplicationRowProps { + application: ISplitTunnelingApplication; + launchApplication: (application: ISplitTunnelingApplication) => void; +} + +function ApplicationRow(props: IApplicationRowProps) { + const [showWarning, setShowWarning] = useState(false); + + const launch = useCallback(() => { + setShowWarning(false); + props.launchApplication(props.application); + }, [props.launchApplication, props.application]); + + const showWarningDialog = useCallback(() => setShowWarning(true), []); + const hideWarningDialog = useCallback(() => setShowWarning(false), []); + + const disabled = props.application.warning === 'launches-elsewhere'; + const warningColor = disabled ? colors.red : colors.yellow; + const warningMessage = disabled + ? sprintf( + messages.pgettext( + 'split-tunneling-view', + '%(applicationName)s is problematic and cannot be excluded from the VPN tunnel.', + ), + { + applicationName: props.application.name, + }, + ) + : sprintf( + messages.pgettext( + 'split-tunneling-view', + '%(applicationName)s is problematic and might not be excluded from the VPN tunnel. Try closing all existing instances of %(applicationName)s before starting it from here.', + ), + { + applicationName: props.application.name, + }, + ); + const warningDialogButtons = disabled + ? [ + <AppButton.BlueButton key="cancel" onClick={hideWarningDialog}> + {messages.gettext('Back')} + </AppButton.BlueButton>, + ] + : [ + <AppButton.BlueButton key="launch" onClick={launch}> + {messages.pgettext('split-tunneling-view', 'Launch')} + </AppButton.BlueButton>, + <AppButton.BlueButton key="cancel" onClick={hideWarningDialog}> + {messages.gettext('Cancel')} + </AppButton.BlueButton>, + ]; + + return ( + <> + <StyledCellButton + onClick={props.application.warning ? showWarningDialog : launch} + lookDisabled={disabled}> + {props.application.icon ? ( + <StyledIcon + source={props.application.icon} + width={35} + height={35} + lookDisabled={disabled} + /> + ) : ( + <StyledIconPlaceholder /> + )} + <StyledCellLabel lookDisabled={disabled}>{props.application.name}</StyledCellLabel> + {props.application.warning && <Cell.Icon source="icon-alert" tintColor={warningColor} />} + </StyledCellButton> + {showWarning && ( + <ModalAlert + type={ModalAlertType.Warning} + iconColor={warningColor} + message={warningMessage} + buttons={warningDialogButtons} + /> + )} + </> + ); +} diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx index ae46c414fc..3db4dcd626 100644 --- a/gui/src/renderer/components/Modal.tsx +++ b/gui/src/renderer/components/Modal.tsx @@ -82,6 +82,7 @@ const ModalAlertButtonContainer = styled.div({ interface IModalAlertProps { type?: ModalAlertType; + iconColor?: string; message?: string; buttons: React.ReactNode[]; children?: React.ReactNode; @@ -152,7 +153,9 @@ export class ModalAlert extends React.Component<IModalAlertProps> { color = colors.red; break; } - return <ImageView height={44} width={44} source={source} tintColor={color} />; + return ( + <ImageView height={44} width={44} source={source} tintColor={this.props.iconColor ?? color} /> + ); } } diff --git a/gui/src/renderer/containers/AdvancedSettingsPage.tsx b/gui/src/renderer/containers/AdvancedSettingsPage.tsx index c9f33f3d63..ddadca983d 100644 --- a/gui/src/renderer/containers/AdvancedSettingsPage.tsx +++ b/gui/src/renderer/containers/AdvancedSettingsPage.tsx @@ -155,6 +155,7 @@ const mapDispatchToProps = (dispatch: ReduxDispatch, props: IAppContext) => { } }, onViewWireguardKeys: () => history.push('/settings/advanced/wireguard-keys'), + onViewLinuxSplitTunneling: () => history.push('/settings/advanced/linux-split-tunneling'), }; }; diff --git a/gui/src/renderer/routes.tsx b/gui/src/renderer/routes.tsx index c3aed29a7c..63a5a3a90e 100644 --- a/gui/src/renderer/routes.tsx +++ b/gui/src/renderer/routes.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Route, RouteComponentProps, Switch, withRouter } from 'react-router'; import Launch from './components/Launch'; +import LinuxSplitTunnelingSettings from './components/LinuxSplitTunnelingSettings'; import TransitionContainer, { TransitionView } from './components/TransitionContainer'; import AccountPage from './containers/AccountPage'; import AdvancedSettingsPage from './containers/AdvancedSettingsPage'; @@ -73,6 +74,11 @@ class AppRoutes extends React.Component<RouteComponentProps, IAppRoutesState> { path="/settings/advanced/wireguard-keys" component={WireguardKeysPage} /> + <Route + exact={true} + path="/settings/advanced/linux-split-tunneling" + component={LinuxSplitTunnelingSettings} + /> <Route exact={true} path="/settings/support" component={SupportPage} /> <Route exact={true} path="/select-location" component={SelectLocationPage} /> </Switch> diff --git a/gui/src/renderer/transitions.ts b/gui/src/renderer/transitions.ts index 23ada748aa..da1a45ac01 100644 --- a/gui/src/renderer/transitions.ts +++ b/gui/src/renderer/transitions.ts @@ -45,6 +45,7 @@ const transitionRules = [ r('/settings', '/settings/preferences', transitions.push), r('/settings', '/settings/advanced', transitions.push), r('/settings/advanced', '/settings/advanced/wireguard-keys', transitions.push), + r('/settings/advanced', '/settings/advanced/linux-split-tunneling', transitions.push), r('/settings', '/settings/support', transitions.push), r(null, '/settings', transitions.slide), r(null, '/select-location', transitions.slide), |
