mirror of
https://github.com/responsively-org/responsively-app
synced 2024-11-14 16:37:27 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
84f900b923
19 changed files with 758 additions and 1 deletions
52
desktop-app/app/actions/networkConfig.js
Normal file
52
desktop-app/app/actions/networkConfig.js
Normal 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);
|
||||
};
|
||||
}
|
27
desktop-app/app/components/ClearNetworkCache/index.js
Normal file
27
desktop-app/app/components/ClearNetworkCache/index.js
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
25
desktop-app/app/components/NetworkConfiguration/index.js
Normal file
25
desktop-app/app/components/NetworkConfiguration/index.js
Normal 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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.label {
|
||||
font-size: 14px;
|
||||
margin-bottom: 5px;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
125
desktop-app/app/components/NetworkThrottling/index.js
Normal file
125
desktop-app/app/components/NetworkThrottling/index.js
Normal 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>
|
||||
);
|
||||
}
|
6
desktop-app/app/components/NetworkThrottling/styles.css
Normal file
6
desktop-app/app/components/NetworkThrottling/styles.css
Normal file
|
@ -0,0 +1,6 @@
|
|||
.networkThrottlingIcon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.throttlingProfileSelectorContainer {
|
||||
margin-bottom: 20px;
|
||||
}
|
|
@ -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;
|
||||
|
|
41
desktop-app/app/components/icons/Network.js
Normal file
41
desktop-app/app/components/icons/Network.js
Normal 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>
|
||||
);
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue