summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-08-14 12:41:50 +0200
committerOskar Nyberg <oskar@mullvad.net>2020-08-14 12:41:50 +0200
commitd94f70ec81d291b5d8d348c40058e25e81e07562 (patch)
treed5bf20348b6c7e502ba4d735f939ff2f618c1d10 /gui/src/renderer
parentfaf933331f93e1a8d771babb31f3ac37a2f9851f (diff)
parent9ecbf3a72e2d4f8a4fb2ce6794beb49cda03bf34 (diff)
downloadmullvadvpn-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.tsx9
-rw-r--r--gui/src/renderer/components/AdvancedSettings.tsx21
-rw-r--r--gui/src/renderer/components/AdvancedSettingsStyles.tsx4
-rw-r--r--gui/src/renderer/components/ImageView.tsx5
-rw-r--r--gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx279
-rw-r--r--gui/src/renderer/components/Modal.tsx5
-rw-r--r--gui/src/renderer/containers/AdvancedSettingsPage.tsx1
-rw-r--r--gui/src/renderer/routes.tsx6
-rw-r--r--gui/src/renderer/transitions.ts1
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),