diff --git a/desktop-app/app/actions/networkConfig.js b/desktop-app/app/actions/networkConfig.js new file mode 100644 index 00000000..7f0aa4a5 --- /dev/null +++ b/desktop-app/app/actions/networkConfig.js @@ -0,0 +1,52 @@ +import pubsub from 'pubsub.js'; +import type {Dispatch, GetState} from '../reducers/types'; +import { + SET_NETWORK_TROTTLING_PROFILE, + CLEAR_NETWORK_CACHE +} from '../constants/pubsubEvents'; + +export const CHANGE_ACTIVE_THROTTLING_PROFILE = 'CHANGE_ACTIVE_THROTTLING_PROFILE'; +export const SAVE_THROTTLING_PROFILES = 'SAVE_THROTTLING_PROFILES'; + +export function changeActiveThrottlingProfile(title='Online') { + return { + type: CHANGE_ACTIVE_THROTTLING_PROFILE, + title, + }; +} + +export function saveThrottlingProfilesList(profiles) { + return { + type: SAVE_THROTTLING_PROFILES, + profiles, + }; +} + +export function onActiveThrottlingProfileChanged(title) { + return (dispatch: Dispatch, getState: GetState) => { + const { + browser: {networkConfiguration: {throttling}}, + } = getState(); + + const activeProfile = throttling.find(x => x.title === title); + + if (activeProfile != null) { + pubsub.publish(SET_NETWORK_TROTTLING_PROFILE, [activeProfile]); + dispatch(changeActiveThrottlingProfile(title)) + } + }; +} + +export function onThrottlingProfilesListChanged(profiles) { + return (dispatch: Dispatch, getState: GetState) => { + dispatch(saveThrottlingProfilesList(profiles)); + const activeProfile = profiles.find(x => x.type === 'Online'); + pubsub.publish(SET_NETWORK_TROTTLING_PROFILE, [activeProfile]); + }; +} + +export function onClearNetworkCache() { + return (dispatch: Dispatch, getState: GetState) => { + pubsub.publish(CLEAR_NETWORK_CACHE); + }; +} diff --git a/desktop-app/app/components/ClearNetworkCache/index.js b/desktop-app/app/components/ClearNetworkCache/index.js new file mode 100644 index 00000000..8cb4ca97 --- /dev/null +++ b/desktop-app/app/components/ClearNetworkCache/index.js @@ -0,0 +1,27 @@ +import React from 'react'; +import cx from 'classnames'; +import Button from '@material-ui/core/Button'; +import CachedIcon from '@material-ui/icons/Cached'; + +import commonStyles from '../common.styles.css'; + +export default function ClearNetworkCache(props) { + return ( +
+
+ Network Cache +
+
+ +
+
+ ); +} diff --git a/desktop-app/app/components/Drawer/index.js b/desktop-app/app/components/Drawer/index.js index 20d564d3..e74027cb 100644 --- a/desktop-app/app/components/Drawer/index.js +++ b/desktop-app/app/components/Drawer/index.js @@ -5,6 +5,7 @@ import cx from 'classnames'; import DeviceDrawerContainer from '../../containers/DeviceDrawerContainer'; import UserPreferencesContainer from '../../containers/UserPreferencesContainer'; import ExtensionsManagerContainer from '../../containers/ExtensionsManagerContainer'; +import NetworkConfigurationContainer from '../../containers/NetworkConfigurationContainer'; import styles from './styles.css'; import commonStyles from '../common.styles.css'; @@ -12,6 +13,7 @@ import { DEVICE_MANAGER, USER_PREFERENCES, EXTENSIONS_MANAGER, + NETWORK_CONFIGURATION, } from '../../constants/DrawerContents'; import {iconsColor} from '../../constants/colors'; import DoubleLeftArrowIcon from '../icons/DoubleLeftArrow'; @@ -70,6 +72,8 @@ function getDrawerContent(type) { return ; case EXTENSIONS_MANAGER: return ; + case NETWORK_CONFIGURATION: + return ; default: return null; } diff --git a/desktop-app/app/components/LeftIconsPane/index.js b/desktop-app/app/components/LeftIconsPane/index.js index b396fb2f..23e20192 100644 --- a/desktop-app/app/components/LeftIconsPane/index.js +++ b/desktop-app/app/components/LeftIconsPane/index.js @@ -6,6 +6,7 @@ import DevicesIcon from '@material-ui/icons/Devices'; import SettingsIcon from '@material-ui/icons/Settings'; import PhotoLibraryIcon from '@material-ui/icons/PhotoLibraryOutlined'; import ExtensionIcon from '@material-ui/icons/Extension'; +import NetworkIcon from '../icons/Network'; import cx from 'classnames'; import Logo from '../icons/Logo'; @@ -17,6 +18,7 @@ import { SCREENSHOT_MANAGER, USER_PREFERENCES, EXTENSIONS_MANAGER, + NETWORK_CONFIGURATION } from '../../constants/DrawerContents'; const LeftIconsPane = props => { @@ -81,6 +83,18 @@ const LeftIconsPane = props => { + toggleState(NETWORK_CONFIGURATION)} + > +
+ +
+
+ + +
+ ); +} diff --git a/desktop-app/app/components/NetworkConfiguration/styles.css b/desktop-app/app/components/NetworkConfiguration/styles.css new file mode 100644 index 00000000..5b879604 --- /dev/null +++ b/desktop-app/app/components/NetworkConfiguration/styles.css @@ -0,0 +1,4 @@ +.label { + font-size: 14px; + margin-bottom: 5px; +} diff --git a/desktop-app/app/components/NetworkThrottling/ProfileManager/index.js b/desktop-app/app/components/NetworkThrottling/ProfileManager/index.js new file mode 100644 index 00000000..48aa58e1 --- /dev/null +++ b/desktop-app/app/components/NetworkThrottling/ProfileManager/index.js @@ -0,0 +1,230 @@ +import React, {useState} from 'react'; +import cx from 'classnames'; +import Button from '@material-ui/core/Button'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import TableFooter from '@material-ui/core/TableFooter'; +import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined'; +import AddCircleOutlineOutlinedIcon from '@material-ui/icons/AddCircleOutlineOutlined'; +import TextField from '@material-ui/core/TextField'; +import NumberFormat from 'react-number-format'; +import InputAdornment from '@material-ui/core/InputAdornment'; + +import commonStyles from '../../common.styles.css'; +import styles from './styles.css'; + +function NumberFormatCustom(props) { + const { inputRef, onChange, ...other } = props; + + return ( + { + onChange({ + target: { + name: props.name, + value: values.floatValue, + }, + }); + }} + allowNegative={false} + decimalScale={0} + /> + ); +} + +export default function ProfileManager({ + profiles, + onSave +}) { + const [currentProfiles, updateProfiles] = useState(profiles); + const [newElement, setNewElement] = useState({type: 'Custom'}); + const [editModeRows, toggleEditModeRows] = useState({}) + + const newElementIsInvalid = (newElement.title != null && (newElement.title.trim() == "" || newElement.title.length > 20 || currentProfiles.filter(x => x.title === newElement.title).length !== 0)); + + const addNewElement = () => { + if (!newElementIsInvalid && newElement.title != null) { + setNewElement({type: 'Custom'}); + updateProfiles([...currentProfiles, newElement]); + } + } + + const removeProfile = (title) => { + updateProfiles(currentProfiles.filter(p => p.title !== title)); + } + + const updateProfile = (title, key, value) => { + const profile = currentProfiles.find(x => x.title === title); + if (profile != null && profile.type === 'Custom') { + profile[key] = value; + updateProfiles([...currentProfiles]); + } + } + + const toggleEditMode = (row) => { + if (row.type !== 'Custom') return; + editModeRows[row.title] = !editModeRows[row.title]; + toggleEditModeRows({...editModeRows}); + } + + return ( +
+ + + + + Name + Download + Upload + Latency + + + + + {currentProfiles.map((row) => ( + + toggleEditMode(row)}> + {row.title} + + + {row.type !== 'Online' && + updateProfile(row.title, 'downloadKps', e.target.value)} + fullWidth + variant="outlined" + placeholder={row.type !== 'Custom' || !editModeRows[row.title] ? "": "(optional)"} + InputProps={{ + inputComponent: NumberFormatCustom, + endAdornment: Kb/s, + }} + disabled={row.type !== 'Custom' || !editModeRows[row.title]} + /> + } + + + {row.type !== 'Online' && + updateProfile(row.title, 'uploadKps', e.target.value)} + fullWidth + variant="outlined" + placeholder={row.type !== 'Custom' || !editModeRows[row.title] ? "": "(optional)"} + InputProps={{ + inputComponent: NumberFormatCustom, + endAdornment: Kb/s, + }} + disabled={row.type !== 'Custom' || !editModeRows[row.title]} + /> + } + + + {row.type !== 'Online' && + updateProfile(row.title, 'latencyMs', e.target.value)} + fullWidth + variant="outlined" + placeholder={row.type !== 'Custom' || !editModeRows[row.title] ? "": "(optional)"} + InputProps={{ + inputComponent: NumberFormatCustom, + endAdornment: ms, + }} + disabled={row.type !== 'Custom' || !editModeRows[row.title]} + /> + } + + + {row.type === 'Custom' && removeProfile(row.title)} />} + + + ))} + +
+
+ + + + + setNewElement({...newElement, title: e.target.value})} + error={newElementIsInvalid} + fullWidth + variant="outlined" + placeholder="New Profile Name" + className={cx(styles.titleField)} + /> + + + setNewElement({...newElement, downloadKps: e.target.value})} + fullWidth + variant="outlined" + placeholder="(optional)" + InputProps={{ + inputComponent: NumberFormatCustom, + endAdornment: Kb/s, + }} + /> + + + setNewElement({...newElement, uploadKps: e.target.value})} + fullWidth + variant="outlined" + placeholder="(optional)" + InputProps={{ + inputComponent: NumberFormatCustom, + endAdornment: Kb/s, + }} + /> + + + setNewElement({...newElement, latencyMs: e.target.value})} + fullWidth + variant="outlined" + placeholder="(optional)" + InputProps={{ + inputComponent: NumberFormatCustom, + endAdornment: ms, + }} + /> + + + + + + + + +
+ ); +} diff --git a/desktop-app/app/components/NetworkThrottling/ProfileManager/styles.css b/desktop-app/app/components/NetworkThrottling/ProfileManager/styles.css new file mode 100644 index 00000000..94fa7993 --- /dev/null +++ b/desktop-app/app/components/NetworkThrottling/ProfileManager/styles.css @@ -0,0 +1,58 @@ +.profileManagerContainer { + height: 600px; + padding: 20px; +} + +.profilesContainer { + max-height: 430px; + overflow-y: auto; + flex-grow: 1; +} + +.profilesHeader > tr > th { + font-size: 20px; + font-weight: bold; +} + +.profilesRow > th, +td { + padding-right: 16px !important; +} + +.profilesRow > th { + cursor: default; +} +.profilesRow > th.customProfile { + cursor: pointer; +} + +.actionIcon { + pointer-events: all; + cursor: pointer; + color: white; +} + +.actionIcon:hover { + color: #6075ef; +} + +.saveButton { + position: absolute !important; + bottom: 25px; + right: 25px; +} + +.numericField * { + text-align: right; + font-size: 14px !important; +} +.numericFieldDisabled fieldset { + border: 0 !important; +} +.numericFieldDisabled input { + color: white !important; +} + +.titleField * { + font-size: 14px !important; +} diff --git a/desktop-app/app/components/NetworkThrottling/index.js b/desktop-app/app/components/NetworkThrottling/index.js new file mode 100644 index 00000000..65dc8738 --- /dev/null +++ b/desktop-app/app/components/NetworkThrottling/index.js @@ -0,0 +1,125 @@ +import React, {useState} from 'react'; +import cx from 'classnames'; +import Button from '@material-ui/core/Button'; +import NetworkCheckIcon from '@material-ui/icons/NetworkCheck'; +import Select from 'react-select'; +import Dialog from '@material-ui/core/Dialog'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import ProfileManager from './ProfileManager'; + +import {makeStyles} from '@material-ui/core/styles'; +import commonStyles from '../common.styles.css'; +import styles from './styles.css'; + +const selectStyles = { + control: selectStyles => ({...selectStyles, backgroundColor: '#ffffff10'}), + option: (selectStyles, {data, isDisabled, isFocused, isSelected}) => { + const color = 'white'; + return { + ...selectStyles, + backgroundColor: isDisabled + ? null + : isSelected + ? '#ffffff40' + : isFocused + ? '#ffffff20' + : null, + color: 'white', + + ':active': { + ...selectStyles[':active'], + backgroundColor: !isDisabled && '#ffffff40', + }, + }; + }, + input: selectStyles => ({...selectStyles}), + placeholder: selectStyles => ({...selectStyles}), + singleValue: (selectStyles, {data}) => ({...selectStyles, color: 'white'}), + menu: selectStyles => ({...selectStyles, background: '#4b4b4b', zIndex: 100}), +}; + +const useStyles = makeStyles(theme => ({ + appBar: { + position: 'relative', + }, + title: { + marginLeft: theme.spacing(2), + flex: 1, + }, +})); + +export default function NetworkThrottling({ + throttling, + onActiveThrottlingProfileChanged, + onThrottlingProfilesListChanged, +}) { + const [open, setOpen] = useState(false); + const closeDialog = () => setOpen(false); + const classes = useStyles(); + + const selectedIdx = throttling.findIndex(p => p.active); + const options = throttling.map(p => { + return { + value: p.title, + label: p.title, + }; + }); + const selectedOption = options[selectedIdx]; + + const onThrottlingProfileChanged = (val) => { + if (val.value !== selectedOption.value) + onActiveThrottlingProfileChanged(val.value); + }; + + const saveThrottlingProfiles = (profiles) => { + onThrottlingProfilesListChanged(profiles); + closeDialog(); + } + + return ( +
+
+ Network Throttling +
+
+
+