import { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { AccessMethodSetting } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { useApiAccessMethodTest } from '../lib/api-access-methods'; import { useHistory } from '../lib/history'; import { generateRoutePath } from '../lib/routeHelpers'; import { RoutePath } from '../lib/routes'; import { useBoolean } from '../lib/utilityHooks'; import { useSelector } from '../redux/store'; import * as Cell from './cell'; import { ContextMenu, ContextMenuContainer, ContextMenuItem, ContextMenuTrigger, } from './ContextMenu'; import ImageView from './ImageView'; import InfoButton from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; import { NavigationBar, NavigationContainer, NavigationInfoButton, NavigationItems, TitleBarItem, } from './NavigationBar'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; import { StyledContent, StyledNavigationScrollbars, StyledSettingsContent } from './SettingsStyles'; import { SmallButton, SmallButtonColor, SmallButtonGroup } from './SmallButton'; const StyledContextMenuButton = styled(Cell.Icon)({ marginRight: '8px', }); const StyledMethodInfoButton = styled(InfoButton)({ marginRight: '11px', }); const StyledSpinner = styled(ImageView)({ height: '10px', width: '10px', marginRight: '6px', }); const StyledNameLabel = styled(Cell.Label)({ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }); const StyledTestResultCircle = styled.div<{ $result: boolean }>((props) => ({ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: props.$result ? colors.green : colors.red, marginRight: '6px', })); // This component is the topmost component in the API access methods view. export default function ApiAccessMethods() { const history = useHistory(); const methods = useSelector((state) => state.settings.apiAccessMethods); const currentMethod = useSelector((state) => state.settings.currentApiAccessMethod); const navigateToEdit = useCallback( (id?: string) => { const path = generateRoutePath(RoutePath.editApiAccessMethods, { id }); history.push(path); }, [history], ); const navigateToNew = useCallback(() => navigateToEdit(), [navigateToEdit]); return ( { // TRANSLATORS: Title label in navigation bar messages.pgettext('navigation-bar', 'API access') } {messages.pgettext('navigation-bar', 'API access')} {messages.pgettext( 'api-access-methods-view', 'Manage and add custom methods to access the Mullvad API.', )} {methods.custom.map((method) => ( ))} {messages.gettext('Add')} ); } interface ApiAccessMethodProps { method: AccessMethodSetting; inUse: boolean; custom?: boolean; } function ApiAccessMethod(props: ApiAccessMethodProps) { const { setApiAccessMethod: setApiAccessMethodImpl, updateApiAccessMethod, removeApiAccessMethod, } = useAppContext(); const history = useHistory(); const [testing, testResult, testApiAccessMethod] = useApiAccessMethodTest(); // State for delete confirmation dialog. const [removeConfirmationVisible, showRemoveConfirmation, hideRemoveConfirmation] = useBoolean(); const confirmRemove = useCallback(() => { void removeApiAccessMethod(props.method.id); hideRemoveConfirmation(); }, [props.method.id]); // Toggle on/off on an access method. const toggle = useCallback( async (value: boolean) => { const updatedMethod = cloneMethod(props.method); updatedMethod.enabled = value; await updateApiAccessMethod(updatedMethod); }, [props.method], ); const setApiAccessMethod = useCallback(async () => { const reachable = await testApiAccessMethod(props.method.id); if (reachable) { await setApiAccessMethodImpl(props.method.id); } }, [testApiAccessMethod, props.method.id]); const menuItems = useMemo>(() => { const items: Array = [ { type: 'item' as const, label: messages.gettext('Use'), disabled: props.inUse, onClick: setApiAccessMethod, }, { type: 'item' as const, label: messages.gettext('Test'), onClick: () => testApiAccessMethod(props.method.id), }, ]; // Edit and Delete shouldn't be available for direct and bridges. if (props.custom) { items.push( { type: 'separator' as const }, { type: 'item' as const, label: messages.gettext('Edit'), onClick: () => history.push( generateRoutePath(RoutePath.editApiAccessMethods, { id: props.method.id }), ), }, { type: 'item' as const, label: messages.gettext('Delete'), onClick: showRemoveConfirmation, }, ); } return items; }, [props.method.id, props.inUse, setApiAccessMethod, testApiAccessMethod, history.push]); return ( {props.method.name} {testing && ( {messages.pgettext('api-access-methods-view', 'Testing...')} )} {!testing && testResult !== undefined && ( {testResult ? messages.pgettext('api-access-methods-view', 'API reachable') : messages.pgettext('api-access-methods-view', 'API unreachable')} )} {!testing && testResult === undefined && props.inUse && ( {messages.pgettext('api-access-methods-view', 'In use')} )} {props.method.type === 'direct' && ( )} {props.method.type === 'bridges' && ( )} {/* Confirmation dialog for method removal */} {messages.gettext('Cancel')} , {messages.gettext('Delete')} , ]} close={hideRemoveConfirmation} title={sprintf(messages.pgettext('api-access-methods-view', 'Delete %(name)s?'), { name: props.method.name, })} message={ props.inUse ? messages.pgettext( 'api-access-methods-view', 'The in use API access method will change.', ) : undefined } /> ); } function cloneMethod(method: T): T { const clonedMethod = { ...method, }; if ( method.type === 'socks5-remote' && clonedMethod.type === 'socks5-remote' && method.authentication !== undefined ) { clonedMethod.authentication = { ...method.authentication }; } return clonedMethod; }