summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/linux-split-tunneling.ts23
-rw-r--r--gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx210
-rw-r--r--gui/src/renderer/components/Modal.tsx5
-rw-r--r--gui/src/shared/linux-split-tunneling-application.ts3
4 files changed, 174 insertions, 67 deletions
diff --git a/gui/src/main/linux-split-tunneling.ts b/gui/src/main/linux-split-tunneling.ts
index f2ea40da13..5114829d1f 100644
--- a/gui/src/main/linux-split-tunneling.ts
+++ b/gui/src/main/linux-split-tunneling.ts
@@ -7,6 +7,11 @@ import path from 'path';
import ISplitTunnelingApplication from '../shared/linux-split-tunneling-application';
import { pascalCaseToCamelCase } from './transform-object-keys';
+const PROBLEMATIC_APPLICATIONS = {
+ launchingInExistingProcess: ['firefox', 'firefox-esr'],
+ launchingElsewhere: ['gnome-terminal'],
+};
+
type DirectoryDescription = string | RegExp;
interface IApplication extends ISplitTunnelingApplication {
@@ -39,6 +44,7 @@ export function getApplications(locale: string): Promise<ISplitTunnelingApplicat
return pascalCaseToCamelCase<IApplication>(applications);
})
.filter(shouldShowApplication)
+ .map(addApplicationWarnings)
.sort((a, b) => a.name.localeCompare(b.name))
.map(async (app) => ({ ...app, icon: await replaceWithIconPath(app.icon) }));
@@ -77,6 +83,23 @@ function shouldShowApplication(application: IApplication): boolean {
);
}
+function addApplicationWarnings(application: IApplication): IApplication {
+ const binaryBasename = path.basename(application.exec!.split(' ')[0]);
+ if (PROBLEMATIC_APPLICATIONS.launchingInExistingProcess.includes(binaryBasename)) {
+ return {
+ ...application,
+ warning: 'launches-in-existing-process',
+ };
+ } else if (PROBLEMATIC_APPLICATIONS.launchingElsewhere.includes(binaryBasename)) {
+ return {
+ ...application,
+ warning: 'launches-elsewhere',
+ };
+ } else {
+ return application;
+ }
+}
+
function localizeNameAndIcon(application: AppData, locale: string) {
if (application.lang) {
// linux-app-list prefixes and suffixes the locale keys with a space for some reason.
diff --git a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
index 81991e83b4..b018221932 100644
--- a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
+++ b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
@@ -1,6 +1,7 @@
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';
@@ -11,6 +12,7 @@ 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,
@@ -47,10 +49,24 @@ const StyledContent = styled.div({
flex: 1,
});
-const StyledIcon = styled(Cell.UntintedIcon)({
+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',
@@ -114,66 +130,68 @@ export default function LinuxSplitTunnelingSettings() {
return (
<>
<StyledPageCover show={browsing} />
- <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>
+ <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>
+ <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>
+ <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>
+ <StyledBrowseButton onClick={launchWithFilePicker}>
+ {messages.pgettext('split-tunneling-view', 'Browse')}
+ </StyledBrowseButton>
+ </StyledContent>
+ </StyledNavigationScrollbars>
+ </NavigationContainer>
+ </StyledContainer>
+ </Layout>
+ </ModalContainer>
</>
);
}
@@ -184,18 +202,78 @@ interface IApplicationRowProps {
}
function ApplicationRow(props: IApplicationRowProps) {
- const onClick = useCallback(() => {
+ 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 (
- <Cell.CellButton onClick={onClick}>
- {props.application.icon ? (
- <StyledIcon source={props.application.icon} width={35} height={35} />
- ) : (
- <StyledIconPlaceholder />
+ <>
+ <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}
+ />
)}
- <Cell.Label>{props.application.name}</Cell.Label>
- </Cell.CellButton>
+ </>
);
}
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/shared/linux-split-tunneling-application.ts b/gui/src/shared/linux-split-tunneling-application.ts
index f75406b301..a96152b70d 100644
--- a/gui/src/shared/linux-split-tunneling-application.ts
+++ b/gui/src/shared/linux-split-tunneling-application.ts
@@ -1,6 +1,9 @@
+type Warning = 'launches-in-existing-process' | 'launches-elsewhere';
+
export default interface ISplitTunnelingApplication {
absolutepath: string;
name: string;
exec: string;
icon?: string;
+ warning?: Warning;
}