Merge remote-tracking branch 'origin/master'

This commit is contained in:
Manoj Vivek 2020-07-22 08:34:05 +05:30
commit 84f900b923
19 changed files with 758 additions and 1 deletions

View file

@ -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);
};
}

View file

@ -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 (
<div className={cx(commonStyles.sidebarContentSection)}>
<div className={cx(commonStyles.sidebarContentSectionTitleBar)}>
<CachedIcon style={{marginRight: 5}} /> Network Cache
</div>
<div className={cx(commonStyles.sidebarContentSectionContainer)}>
<Button
variant="contained"
color="primary"
aria-label="clear network cache"
component="span"
onClick={() => props.onClearNetworkCache()}
>
Clear Network Cache
</Button>
</div>
</div>
);
}

View file

@ -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 <UserPreferencesContainer />;
case EXTENSIONS_MANAGER:
return <ExtensionsManagerContainer />;
case NETWORK_CONFIGURATION:
return <NetworkConfigurationContainer />;
default:
return null;
}

View file

@ -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 => {
<ExtensionIcon {...iconProps} className="extensionsIcon" />
</div>
</Grid>
<Grid
item
className={cx(commonStyles.icons, styles.icon, commonStyles.enabled, {
[commonStyles.selected]:
props.drawer.open && props.drawer.content === NETWORK_CONFIGURATION,
})}
onClick={() => toggleState(NETWORK_CONFIGURATION)}
>
<div>
<NetworkIcon {...iconProps} color='white' className="networkIcon" />
</div>
</Grid>
</Grid>
<div style={{position: 'relative'}}>
<div

View file

@ -0,0 +1,25 @@
import React from 'react';
import cx from 'classnames';
import ClearNetworkCache from '../ClearNetworkCache'
import NetworkThrottling from '../NetworkThrottling'
import styles from './styles.css';
import commonStyles from '../common.styles.css';
export default function DeviceDrawer({
throttling,
onActiveThrottlingProfileChanged,
onThrottlingProfilesListChanged,
onClearNetworkCache
}) {
return (
<div>
<ClearNetworkCache onClearNetworkCache={onClearNetworkCache} />
<NetworkThrottling
throttling={throttling}
onActiveThrottlingProfileChanged={onActiveThrottlingProfileChanged}
onThrottlingProfilesListChanged={onThrottlingProfilesListChanged}
/>
</div>
);
}

View file

@ -0,0 +1,4 @@
.label {
font-size: 14px;
margin-bottom: 5px;
}

View file

@ -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 (
<NumberFormat
{...other}
getInputRef={inputRef}
onValueChange={(values) => {
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 (
<div className={cx(styles.profileManagerContainer)}>
<TableContainer className={cx(styles.profilesContainer)}>
<Table size="small">
<TableHead className={cx(styles.profilesHeader)}>
<TableRow>
<TableCell style={{ width: "32%" }}>Name</TableCell>
<TableCell style={{ width: "21%" }} align="right">Download</TableCell>
<TableCell style={{ width: "21%" }} align="right">Upload</TableCell>
<TableCell style={{ width: "21%" }} align="right">Latency</TableCell>
<TableCell style={{ width: "5%" }} align="right"/>
</TableRow>
</TableHead>
<TableBody>
{currentProfiles.map((row) => (
<TableRow key={row.title} className={cx(styles.profilesRow)}>
<TableCell component="th" scope="row" className={cx({[styles.customProfile]: row.type === 'Custom'})} onClick={() => toggleEditMode(row)}>
{row.title}
</TableCell>
<TableCell align="right">
{row.type !== 'Online' &&
<TextField
className={cx(styles.numericField, {[styles.numericFieldDisabled]: row.type !== 'Custom' || !editModeRows[row.title]})}
value={row.downloadKps == null ? '' : row.downloadKps}
onChange={(e) => updateProfile(row.title, 'downloadKps', e.target.value)}
fullWidth
variant="outlined"
placeholder={row.type !== 'Custom' || !editModeRows[row.title] ? "": "(optional)"}
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: <InputAdornment position="end">Kb/s</InputAdornment>,
}}
disabled={row.type !== 'Custom' || !editModeRows[row.title]}
/>
}
</TableCell>
<TableCell align="right">
{row.type !== 'Online' &&
<TextField
className={cx(styles.numericField, {[styles.numericFieldDisabled]: row.type !== 'Custom' || !editModeRows[row.title]})}
value={row.uploadKps == null ? '' : row.uploadKps}
onChange={(e) => updateProfile(row.title, 'uploadKps', e.target.value)}
fullWidth
variant="outlined"
placeholder={row.type !== 'Custom' || !editModeRows[row.title] ? "": "(optional)"}
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: <InputAdornment position="end">Kb/s</InputAdornment>,
}}
disabled={row.type !== 'Custom' || !editModeRows[row.title]}
/>
}
</TableCell>
<TableCell align="right">
{row.type !== 'Online' &&
<TextField
className={cx(styles.numericField, {[styles.numericFieldDisabled]: row.type !== 'Custom' || !editModeRows[row.title]})}
value={row.latencyMs == null ? '' : row.latencyMs}
onChange={(e) => updateProfile(row.title, 'latencyMs', e.target.value)}
fullWidth
variant="outlined"
placeholder={row.type !== 'Custom' || !editModeRows[row.title] ? "": "(optional)"}
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: <InputAdornment position="end">ms</InputAdornment>,
}}
disabled={row.type !== 'Custom' || !editModeRows[row.title]}
/>
}
</TableCell>
<TableCell align="right">
{row.type === 'Custom' && <CancelOutlinedIcon className={cx(styles.actionIcon)} onClick={() => removeProfile(row.title)} />}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TableFooter component={Table}>
<TableHead className={cx(styles.profilesHeader)}>
<TableRow>
<TableCell style={{ width: "32%" }}>
<TextField
autoFocus
value={newElement.title || ''}
onChange={(e) => setNewElement({...newElement, title: e.target.value})}
error={newElementIsInvalid}
fullWidth
variant="outlined"
placeholder="New Profile Name"
className={cx(styles.titleField)}
/>
</TableCell>
<TableCell style={{ width: "21%" }} align="right">
<TextField
className={cx(styles.numericField)}
value={newElement.downloadKps == null ? '' : newElement.downloadKps}
onChange={(e) => setNewElement({...newElement, downloadKps: e.target.value})}
fullWidth
variant="outlined"
placeholder="(optional)"
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: <InputAdornment position="end">Kb/s</InputAdornment>,
}}
/>
</TableCell>
<TableCell style={{ width: "21%" }} align="right">
<TextField
className={cx(styles.numericField)}
value={newElement.uploadKps == null ? '' : newElement.uploadKps}
onChange={(e) => setNewElement({...newElement, uploadKps: e.target.value})}
fullWidth
variant="outlined"
placeholder="(optional)"
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: <InputAdornment position="end">Kb/s</InputAdornment>,
}}
/>
</TableCell>
<TableCell style={{ width: "21%" }} align="right">
<TextField
className={cx(styles.numericField)}
value={newElement.latencyMs == null ? '' : newElement.latencyMs}
onChange={(e) => setNewElement({...newElement, latencyMs: e.target.value})}
fullWidth
variant="outlined"
placeholder="(optional)"
InputProps={{
inputComponent: NumberFormatCustom,
endAdornment: <InputAdornment position="end">ms</InputAdornment>,
}}
/>
</TableCell>
<TableCell style={{ width: "5%" }} align="right">
<AddCircleOutlineOutlinedIcon className={cx(styles.actionIcon)} onClick={addNewElement} />
</TableCell>
</TableRow>
</TableHead>
</TableFooter>
<Button
variant="contained"
color="primary"
aria-label="clear network cache"
component="span"
onClick={() => onSave(currentProfiles)}
size="large"
className={cx(styles.saveButton)}
>
Save
</Button>
</div>
);
}

View file

@ -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;
}

View file

@ -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 (
<div className={cx(commonStyles.sidebarContentSection)}>
<div className={cx(commonStyles.sidebarContentSectionTitleBar)}>
<NetworkCheckIcon className={cx(styles.networkThrottlingIcon)}/> Network Throttling
</div>
<div className={cx(commonStyles.sidebarContentSectionContainer)}>
<div className={cx(styles.throttlingProfileSelectorContainer)}>
<Select
options={options}
value={selectedOption}
onChange={onThrottlingProfileChanged}
styles={selectStyles}
/>
</div>
<Button
variant="contained"
color="primary"
aria-label="clear network cache"
component="span"
onClick={() => setOpen(true)}
>
Manage Profiles
</Button>
<Dialog className={cx(styles.profileManagerDialog)} maxWidth="md" fullWidth open={open} scroll="paper" onClose={closeDialog}>
<AppBar className={classes.appBar} color="secondary">
<Toolbar>
<Typography variant="h6" className={classes.title}>
Manage Throttling Profiles
</Typography>
<Button color="inherit" onClick={closeDialog}>
close
</Button>
</Toolbar>
</AppBar>
<DialogContent>
<ProfileManager profiles={[...throttling]} onSave={saveThrottlingProfiles} />
</DialogContent>
</Dialog>
</div>
</div>
);
}

View file

@ -0,0 +1,6 @@
.networkThrottlingIcon {
margin-right: 5px;
}
.throttlingProfileSelectorContainer {
margin-bottom: 20px;
}

View file

@ -28,6 +28,8 @@ import {
DELETE_STORAGE,
ADDRESS_CHANGE,
STOP_LOADING,
CLEAR_NETWORK_CACHE,
SET_NETWORK_TROTTLING_PROFILE,
} from '../../constants/pubsubEvents';
import {CAPABILITIES} from '../../constants/devices';
@ -77,6 +79,7 @@ class WebView extends Component {
address: this.props.browser.address,
};
this.subscriptions = [];
this.dbg = null;
}
componentDidMount() {
@ -141,8 +144,18 @@ class WebView extends Component {
)
);
this.subscriptions.push(
pubsub.subscribe(SET_NETWORK_TROTTLING_PROFILE, this.setNetworkThrottlingProfile)
);
this.subscriptions.push(
pubsub.subscribe(CLEAR_NETWORK_CACHE, this.clearNetworkCache)
);
this.webviewRef.current.addEventListener('dom-ready', () => {
this.initEventTriggers(this.webviewRef.current);
this.dbg = this.getWebContents().debugger;
if (!this.dbg.isAttached())
this.dbg.attach();
});
if (this.props.transmitNavigatorStatus) {
@ -248,6 +261,8 @@ class WebView extends Component {
componentWillUnmount() {
this.subscriptions.forEach(pubsub.unsubscribe);
if (this.dbg && this.dbg.isAttached())
this.dbg.detach();
}
initDeviceEmulationParams = () => {
@ -397,6 +412,44 @@ class WebView extends Component {
this.webviewRef.current.send('disableInspectorMessage');
};
setNetworkThrottlingProfile = ({
type,
downloadKps,
uploadKps,
latencyMs,
}) => {
// TODO : change this when https://github.com/electron/electron/issues/21250 is solved
// if (type === 'Online') {
// this.getWebContents().session.disableNetworkEmulation();
// } else if (type === 'Offline') {
// this.getWebContents().session.enableNetworkEmulation({offline: true});
// } else if (type === 'Custom') {
// const downloadThroughput = downloadKps != null? downloadKps * 128 : undefined;
// const uploadThroughput = uploadKps != null? uploadKps * 128 : undefined;
// this.getWebContents().session.enableNetworkEmulation({offline: false, latency: latencyMs, downloadThroughput, uploadThroughput });
// }
// WORKAROUND
if (type === 'Online') {
this.dbg.sendCommand('Network.disable');
} else if (type === 'Offline') {
this.dbg.sendCommand('Network.enable').then(_ => {
this.dbg.sendCommand('Network.emulateNetworkConditions', {offline: true, latency: 0, downloadThroughput: -1, uploadThroughput: -1});
});
} else {
const downloadThroughput = downloadKps != null? downloadKps * 128 : -1;
const uploadThroughput = uploadKps != null? uploadKps * 128 : -1;
const latency = latencyMs || 0;
this.dbg.sendCommand('Network.enable').then(_ => {
this.dbg.sendCommand('Network.emulateNetworkConditions', {offline: false, latency, downloadThroughput, uploadThroughput});
});
}
}
clearNetworkCache = () => {
this.getWebContents().session.clearCache();
}
messageHandler = ({channel: type, args: [message]}) => {
if (type !== MESSAGE_TYPES.toggleEventMirroring && this.state.isUnplugged) {
return;
@ -442,7 +495,7 @@ class WebView extends Component {
bsScript.src = '${BROWSER_SYNC_EMBED_SCRIPT}';
bsScript.async = true;
document.body.appendChild(bsScript);
responsivelyApp.deviceId = '${this.props.device.id}';
document.addEventListener('mouseleave', () => {
window.responsivelyApp.mouseOn = false;

View file

@ -0,0 +1,41 @@
import React, {Fragment} from 'react';
export default ({width, height, color, padding, margin}) => (
<Fragment>
<svg
height={height}
width={width}
fill={color}
style={{padding, margin}}
viewBox="0 0 269.393 269.393"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
className="muteIcon"
xmlSpace="preserve"
>
<path d="M134.696,0C60.424,0,0,60.425,0,134.696s60.424,134.696,134.696,134.696s134.696-60.425,134.696-134.696
S208.968,0,134.696,0z M136.869,252.518c-12.91,0-25.84-8.779-36.409-24.721c-4.421-6.669-8.316-14.428-11.614-22.979h38.351v20.879
c0,4.143,3.358,7.5,7.5,7.5s7.5-3.357,7.5-7.5v-20.879h42.616C173.729,233.559,156.188,252.518,136.869,252.518z M15.242,142.196
H39.23c2.921,11.683,12.5,20.75,24.457,22.928c1.12,8.63,2.689,16.884,4.66,24.693H28.471
C20.961,175.403,16.303,159.28,15.242,142.196z M142.196,17.372c13.801,2.521,26.969,14.745,37.083,34.544
c-3.816,3.294-6.688,7.649-8.15,12.607h-28.934V17.372z M196.607,83.585c-6.375,0-11.563-5.187-11.563-11.563
s5.187-11.563,11.563-11.563s11.563,5.187,11.563,11.563S202.983,83.585,196.607,83.585z M171.13,79.522
c3.01,10.206,11.992,17.875,22.887,18.934c1.574,9.312,2.518,18.938,2.841,28.74h-54.661V79.522H171.13z M127.196,127.196H99.257
c-2.597-10.386-10.455-18.707-20.568-21.958c1.209-8.956,2.981-17.578,5.238-25.716h43.269V127.196z M69.244,150.634
c-8.788,0-15.938-7.149-15.938-15.938s7.149-15.938,15.938-15.938s15.938,7.149,15.938,15.938S78.032,150.634,69.244,150.634z
M63.697,104.267c-11.961,2.175-21.545,11.243-24.467,22.93H15.242c1.063-17.104,5.731-33.247,13.257-47.674H68.42
C66.401,87.448,64.81,95.734,63.697,104.267z M78.7,164.15c10.108-3.253,17.961-11.572,20.557-21.954h27.939v47.621h-43.29
C81.657,181.696,79.906,173.082,78.7,164.15z M142.196,189.817v-47.621h54.667c-0.576,17.011-3.092,33.174-7.111,47.621H142.196z
M211.869,142.196h18.328c4.142,0,7.5-3.357,7.5-7.5s-3.358-7.5-7.5-7.5h-18.331c-0.332-10.757-1.371-21.329-3.116-31.561
c6.386-3.297,11.269-9.105,13.336-16.113h18.809c8.618,16.521,13.499,35.287,13.499,55.174c0,19.866-4.871,38.614-13.471,55.121
h-35.547C209.143,174.965,211.356,158.863,211.869,142.196z M231.602,64.522h-9.517c-3.245-11.005-13.435-19.063-25.477-19.063
c-1.242,0-2.463,0.092-3.66,0.258c-4.421-8.751-9.442-16.33-14.927-22.601C199.615,31.531,218.182,46.041,231.602,64.522z
M127.196,15.242v49.281H88.854c3.211-8.316,6.994-15.876,11.277-22.426c2.267-3.467,1.294-8.115-2.173-10.382
c-3.467-2.268-8.115-1.293-10.382,2.173c-5.761,8.811-10.693,19.19-14.666,30.635H37.791
C58.186,36.436,90.473,17.524,127.196,15.242z M37.752,204.817H72.81c6.236,18.028,14.801,32.962,24.934,43.73
C73.41,240.63,52.477,225.118,37.752,204.817z M178.108,246.242c4.755-5.473,9.176-11.948,13.18-19.369
c3.662-6.787,6.855-14.192,9.587-22.056h30.765C218.235,223.299,199.686,237.815,178.108,246.242z"/>
</svg>
</Fragment>
);

View file

@ -2,3 +2,4 @@ export const DEVICE_MANAGER = 'DEVICE_MANAGER';
export const SCREENSHOT_MANAGER = 'SCREENSHOT_MANAGER';
export const USER_PREFERENCES = 'USER_PREFERENCES';
export const EXTENSIONS_MANAGER = 'EXTENSIONS_MANAGER';
export const NETWORK_CONFIGURATION = 'NETWORK_CONFIGURATION';

View file

@ -13,5 +13,8 @@ export const ENABLE_INSPECTOR_ALL_DEVICES = 'ENABLE_INSPECTOR_ALL_DEVICES';
export const DISABLE_INSPECTOR_ALL_DEVICES = 'DISABLE_INSPECTOR_ALL_DEVICES';
export const STOP_LOADING = 'STOP_LOADING';
export const SET_NETWORK_TROTTLING_PROFILE = 'SET_NETWORK_TROTTLING_PROFILE';
export const CLEAR_NETWORK_CACHE = 'CLEAR_NETWORK_CACHE';
// status bar events
export const STATUS_BAR_VISIBILITY_CHANGE = 'status-bar-visibility-change';

View file

@ -1,5 +1,6 @@
export const ACTIVE_DEVICES = 'activeDevices';
export const CUSTOM_DEVICES = 'customDevices';
export const USER_PREFERENCES = 'userPreferences';
export const NETWORK_CONFIGURATION = 'networkConfiguration';
export const BOOKMARKS = 'bookmarks';
export const STATUS_BAR_VISIBILITY = 'statusBarVisibility';

View file

@ -0,0 +1,20 @@
// @flow
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import NetworkConfiguration from '../../components/NetworkConfiguration';
import * as NetworkConfigActions from '../../actions/networkConfig';
function mapStateToProps(state) {
return {
throttling: state.browser.networkConfiguration.throttling,
// proxy: state.browser.networkConfiguration.proxy,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(NetworkConfigActions, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(NetworkConfiguration);

View file

@ -27,6 +27,10 @@ import {
TOGGLE_ALL_DEVICES_MUTED,
TOGGLE_DEVICE_MUTED,
} from '../actions/browser';
import {
CHANGE_ACTIVE_THROTTLING_PROFILE,
SAVE_THROTTLING_PROFILES,
} from '../actions/networkConfig'
import type {Action} from './types';
import getAllDevices from '../constants/devices';
import type {Device} from '../constants/devices';
@ -40,6 +44,7 @@ import {
ACTIVE_DEVICES,
USER_PREFERENCES,
CUSTOM_DEVICES,
NETWORK_CONFIGURATION,
} from '../constants/settingKeys';
import {
getHomepage,
@ -120,6 +125,20 @@ type FilterFieldType = FILTER_FIELDS.OS | FILTER_FIELDS.DEVICE_TYPE;
type FilterType = {[key: FilterFieldType]: Array<string>};
type NetworkThrottlingProfileType = {
type: 'Online' | 'Offline' | 'Preset' | 'Custom',
title: string,
downloadKps: number,
uploadKps: number,
latencyMs: number,
active: boolean
};
type NetworkConfigurationType = {
throttling: NetworkThrottlingProfileType[],
// proxy: NetworkProxyProfileType[],
};
export type BrowserStateType = {
devices: Array<Device>,
homepage: string,
@ -137,6 +156,7 @@ export type BrowserStateType = {
isInspecting: boolean,
windowSize: WindowSizeType,
allDevicesMuted: boolean,
networkConfiguration: NetworkConfigurationType,
};
let _activeDevices = null;
@ -231,6 +251,54 @@ function _updateFileWatcher(newURL) {
else ipcRenderer.send('stop-watcher');
}
function getDefaultNetworkThrottlingProfiles(): NetworkThrottlingProfileType[] {
return [
{
type: 'Online',
title: 'Online',
active: true,
},
{
type: 'Offline',
title: 'Offline',
downloadKps: 0,
uploadKps: 0,
latencyMs: 0
},
// https://github.com/ChromeDevTools/devtools-frontend/blob/4f404fa8beab837367e49f68e29da427361b1f81/front_end/sdk/NetworkManager.js#L251-L265
{
type: 'Preset',
title: 'Slow 3G',
downloadKps: 400,
uploadKps: 400,
latencyMs: 2000
},
{
type: 'Preset',
title: 'Fast 3G',
downloadKps: 1475,
uploadKps: 675,
latencyMs: 563
}
]
}
function _getNetworkConfiguration(): NetworkConfigurationType {
const ntwrk: NetworkConfigurationType = settings.get(NETWORK_CONFIGURATION) || {};
if (ntwrk.throttling == null)
ntwrk.throttling = getDefaultNetworkThrottlingProfiles();
// if (ntwrk.proxy == null)
// ntwrk.proxy = getDefaultNetworkProxyProfiles();
return ntwrk;
}
function _setNetworkConfiguration(networkConfiguration: NetworkConfigurationType) {
settings.set(NETWORK_CONFIGURATION, networkConfiguration);
}
export default function browser(
state: BrowserStateType = {
devices: _getActiveDevices(),
@ -271,6 +339,7 @@ export default function browser(
isInspecting: false,
windowSize: getWindowSize(),
allDevicesMuted: false,
networkConfiguration: _getNetworkConfiguration(),
},
action: Action
) {
@ -387,6 +456,22 @@ export default function browser(
allDevicesMuted: state.devices.every(x => x.isMuted),
devices: [...state.devices],
};
case CHANGE_ACTIVE_THROTTLING_PROFILE:
const throttling = state.networkConfiguration.throttling
const activeProfile = throttling.find(x => x.title === action.title);
if (activeProfile != null) {
throttling.forEach(x => x.active = false);
activeProfile.active = true;
}
return {...state, networkConfiguration: {...state.networkConfiguration, throttling: [...throttling]}}
case SAVE_THROTTLING_PROFILES:
action.profiles.forEach(x => x.active = false);
action.profiles[0].active = true;
_setNetworkConfiguration({
...state.networkConfiguration,
throttling: action.profiles,
});
return {...state, networkConfiguration: {...state.networkConfiguration, throttling: action.profiles}}
default:
return state;
}

View file

@ -291,6 +291,7 @@
"react-beautiful-dnd": "^11.0.5",
"react-dom": "^16.12.0",
"react-hot-loader": "^4.8",
"react-number-format": "^4.4.1",
"react-redux": "^7.1.0",
"react-resizable": "^1.10.1",
"react-router": "^5.0.1",

View file

@ -13131,6 +13131,13 @@ react-node-resolver@^2.0.1:
resolved "https://registry.yarnpkg.com/react-node-resolver/-/react-node-resolver-2.0.1.tgz#1f0cc83938bf590a1cf42006b23f6b8f68e7b886"
integrity sha512-+PPy/FtAAo5wsLQYMlHkxJ3AMUGL33gpEIx/HBzS8OrcIfacRhGaNVWUJ8bhEbc64en+/bbCNTVZR+pkhqXEbA==
react-number-format@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-number-format/-/react-number-format-4.4.1.tgz#d5614dd25edfc21ed48b97356213440081437a94"
integrity sha512-ZGFMXZ0U7DcmQ3bSZY3FULOA1mfqreT9NIMYZNoa/ouiSgiTQiYA95Uj2KN8ge6BRr+ghA5vraozqWqsHZQw3Q==
dependencies:
prop-types "^15.7.2"
react-redux@^7.0.3, react-redux@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"