Bug 1358414 - Introduce column resizer in request list; r=Honza

Adding feature to netmonitor for resizing of columns. In this patch the functionality is hidden behind the pref devtools.netmonitor.features.resizeColumns.  This feature is currently turned off - false.

Differential Revision: https://phabricator.services.mozilla.com/D22719

--HG--
extra : moz-landing-system : lando
This commit is contained in:
lenka 2019-03-08 15:42:54 +00:00
Родитель b9a96f34ff
Коммит f098dc0c1e
18 изменённых файлов: 815 добавлений и 191 удалений

Просмотреть файл

@ -15,6 +15,7 @@ const {
SELECT_DETAILS_PANEL_TAB,
TOGGLE_COLUMN,
WATERFALL_RESIZE,
SET_COLUMNS_WIDTH,
} = require("../constants");
const { getDisplayedRequests } = require("../selectors/index");
@ -137,6 +138,18 @@ function toggleColumn(column) {
};
}
/**
* Set width of multiple columns
*
* @param {array} widths - array of pairs {name, width}
*/
function setColumnsWidth(widths) {
return {
type: SET_COLUMNS_WIDTH,
widths,
};
}
/**
* Toggle network details panel.
*/
@ -179,6 +192,7 @@ module.exports = {
resizeWaterfall,
selectDetailsPanelTab,
toggleColumn,
setColumnsWidth,
toggleNetworkDetails,
togglePersistentLogs,
toggleBrowserCache,

Просмотреть файл

@ -7,7 +7,7 @@
.request-list-empty-notice {
margin: 0;
flex: 1;
overflow: auto;
overflow-x: hidden;
}
.empty-notice-element {
@ -59,16 +59,18 @@
.requests-list-table {
/* Reset default browser style of <table> */
border-spacing: 0;
width: 100%;
/* The layout must be fixed for resizing of columns to work.
The layout is based on the first row.
Set the width of those cells, and the rest of the table follows. */
table-layout: fixed;
}
.requests-list-column {
cursor: default;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
max-width: 50px;
min-width: 50px;
/* Reset default browser style of <td> */
padding: 0;
@ -108,11 +110,11 @@
transparent 85%) 1 1;
border-width: 0;
border-inline-start-width: 1px;
padding-inline-start: 16px;
width: 100%;
min-height: 23px;
text-align: center;
color: inherit;
padding: 1px 4px;
}
.requests-list-header-button::-moz-focus-inner {
@ -129,7 +131,7 @@
text-align: center;
vertical-align: middle;
/* Align button text to center */
width: calc(100% - 8px);
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
@ -162,13 +164,32 @@
border-image: linear-gradient(var(--theme-splitter-color), var(--theme-splitter-color)) 1 1;
}
/* Requests list headers column-resizer */
.requests-list-headers .column-resizer {
z-index: 1000;
cursor: ew-resize;
margin-left: -3px;
width: 7px;
min-height: 23px;
position: absolute;
background-color: transparent;
}
/**
* Make sure headers are not processing any mouse
* events. This is good for performance during dragging.
*/
.requests-list-headers.dragging {
pointer-events: none;
}
/* Requests list column */
/* Status column */
.requests-list-status {
min-width: 70px;
/* Don't ellipsize status codes */
/* Don't ellipsize status codes */
text-overflow: initial;
}
@ -226,18 +247,6 @@
transform: rotate(45deg);
}
/* Method column */
.requests-list-method {
min-width: 85px;
}
/* File column */
.requests-list-file {
width: 40%;
}
.requests-list-file.requests-list-column {
text-align: start;
}
@ -246,73 +255,6 @@
filter: brightness(1.3);
}
/* Protocol column */
.requests-list-protocol {
width: 8%;
}
/* Cookies column */
.requests-list-cookies {
width: 6%;
}
/* Set Cookies column */
.requests-list-set-cookies {
width: 8%;
}
/* Scheme column */
.requests-list-scheme {
width: 8%;
}
/* Start Time column */
.requests-list-start-time {
width: 8%;
}
/* End Time column */
.requests-list-end-time {
width: 8%;
}
/* Response Time column */
.requests-list-response-time {
width: 10%;
}
/* Duration column */
.requests-list-duration-time {
width: 8%;
}
/* Latency column */
.requests-list-latency-time {
width: 8%;
}
/* Response header columns */
.requests-list-response-header {
width: 10%;
}
/* Domain column */
.requests-list-domain {
min-width: 100px;
width: 30%;
}
.requests-list-domain.requests-list-column {
text-align: start;
}
@ -367,18 +309,6 @@
filter: brightness(500%);
}
/* RemoteIP column */
.requests-list-remoteip {
width: 9%;
}
/* Cause column */
.requests-list-cause {
min-width: 75px;
}
.request-list-item .requests-list-cause.requests-list-column {
padding-left: 5px;
}
@ -396,30 +326,9 @@
margin-inline-end: 3px;
}
/* Type column */
.requests-list-type {
min-width: 65px;
}
/* Transferred column */
.requests-list-transferred {
min-width: 110px;
}
/* Size column */
.requests-list-size {
min-width: 80px;
}
/* Waterfall column */
.requests-list-waterfall {
width: 25vw;
max-width: 25vw;
min-width: 25vw;
background-repeat: repeat-y;
background-position: left center;
/* Background created on a <canvas> in js. */
@ -572,10 +481,6 @@
/* Responsive web design support */
@media (max-width: 700px) {
.requests-list-header-button {
padding-inline-start: 8px;
}
.requests-list-status-code {
width: auto;
}

Просмотреть файл

@ -123,6 +123,10 @@ class RequestListContent extends Component {
window.removeEventListener("resize", this.onResize);
}
/*
* Removing onResize() method causes perf regression - too many repaints of the panel.
* So it is needed in ComponentDidMount and ComponentDidUpdate. See Bug 1532914.
*/
onResize() {
const parent = this.refs.scrollEl.parentNode;
this.refs.scrollEl.style.width = parent.offsetWidth + "px";

Просмотреть файл

@ -15,7 +15,6 @@ const { getPerformanceAnalysisURL } = require("../utils/mdn-utils");
// Components
const MDNLink = createFactory(require("devtools/client/shared/components/MdnLink"));
const RequestListHeader = createFactory(require("./RequestListHeader"));
const { button, div, span } = dom;
@ -45,7 +44,6 @@ class RequestListEmptyNotice extends Component {
{
className: "request-list-empty-notice",
},
RequestListHeader(),
div({ className: "notice-reload-message empty-notice-element" },
span(null, RELOAD_NOTICE_1),
button(

Просмотреть файл

@ -4,22 +4,36 @@
"use strict";
const { Component } = require("devtools/client/shared/vendor/react");
const Services = require("Services");
const { createRef, Component, createFactory } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { connect } = require("devtools/client/shared/redux/visibility-handler-connect");
const { getTheme, addThemeObserver, removeThemeObserver } =
require("devtools/client/shared/theme");
const {
getTheme,
addThemeObserver,
removeThemeObserver,
} = require("devtools/client/shared/theme");
const Actions = require("../actions/index");
const { HEADERS, REQUESTS_WATERFALL } = require("../constants");
const {
HEADERS,
REQUESTS_WATERFALL,
MIN_COLUMN_WIDTH,
DEFAULT_COLUMN_WIDTH,
} = require("../constants");
const { getWaterfallScale } = require("../selectors/index");
const { getFormattedTime } = require("../utils/format-utils");
const { L10N } = require("../utils/l10n");
const RequestListHeaderContextMenu = require("../widgets/RequestListHeaderContextMenu");
const WaterfallBackground = require("../widgets/WaterfallBackground");
const Draggable = createFactory(require("devtools/client/shared/components/splitter/Draggable"));
const { div, button } = dom;
// Support for columns resizing is currently hidden behind this pref.
const RESIZE_COLUMNS =
Services.prefs.getBoolPref("devtools.netmonitor.features.resizeColumns");
/**
* Render the request list header with sorting arrows for columns.
* Displays tick marks in the waterfall column header.
@ -36,11 +50,15 @@ class RequestListHeader extends Component {
sortBy: PropTypes.func.isRequired,
toggleColumn: PropTypes.func.isRequired,
waterfallWidth: PropTypes.number,
columnsData: PropTypes.object.isRequired,
setColumnsWidth: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.requestListHeader = createRef();
this.onContextMenu = this.onContextMenu.bind(this);
this.drawBackground = this.drawBackground.bind(this);
this.resizeWaterfall = this.resizeWaterfall.bind(this);
@ -60,6 +78,10 @@ class RequestListHeader extends Component {
// Create the object that takes care of drawing the waterfall canvas background
this.background = new WaterfallBackground(document);
this.drawBackground();
// When visible columns add up to less or more than 100% => update widths in prefs.
if (this.shouldUpdateWidths()) {
this.updateColumnsWidth();
}
this.resizeWaterfall();
window.addEventListener("resize", this.resizeWaterfall);
addThemeObserver(this.drawBackground);
@ -67,6 +89,12 @@ class RequestListHeader extends Component {
componentDidUpdate() {
this.drawBackground();
// check if the widths in prefs need to be updated
// e.g. after hide/show column
if (this.shouldUpdateWidths()) {
this.updateColumnsWidth();
this.resizeWaterfall();
}
}
componentWillUnmount() {
@ -163,54 +191,376 @@ class RequestListHeader extends Component {
return div({ className }, label);
}
render() {
const { columns, scale, sort, sortBy, waterfallWidth } = this.props;
// Dragging Events
/**
* Set 'resizing' cursor on entire container dragging.
* This avoids cursor-flickering when the mouse leaves
* the column-resizer area (happens frequently).
*/
onStartMove() {
// Set cursor to dragging
const container = document.querySelector(".request-list-container");
container.style.cursor = "ew-resize";
// Class .dragging is used to disable pointer events while dragging - see css.
this.requestListHeader.classList.add("dragging");
}
/**
* A handler that calculates the new width of the columns
* based on mouse position and adjusts the width.
*/
onMove(name, x) {
const parentEl = document.querySelector(".requests-list-headers");
const parentWidth = parentEl.getBoundingClientRect().width;
// Get the current column handle and save its old width
// before changing so we can compute the adjustment in width
const headerRef = this.refs[`${name}Header`];
const headerRefRect = headerRef.getBoundingClientRect();
const oldWidth = headerRefRect.width;
// Get the column handle that will compensate the width change.
const compensateHeaderName = this.getCompensateHeader();
if (name === compensateHeaderName) {
// this is the case where we are resizing waterfall
this.moveWaterfall(x, parentWidth);
return;
}
const compensateHeaderRef = this.refs[`${compensateHeaderName}Header`];
const compensateHeaderRefRect = compensateHeaderRef.getBoundingClientRect();
const oldCompensateWidth = compensateHeaderRefRect.width;
const sumOfBothColumns = oldWidth + oldCompensateWidth;
// Get minimal widths for both changed columns (in px).
const minWidth = this.getMinWidth(name);
const minCompensateWidth = this.getMinWidth(compensateHeaderName);
// Calculate new width (according to the mouse x-position) and set to style.
// Do not allow to set it below minWidth.
const newWidth = Math.max(x - headerRefRect.left, minWidth);
headerRef.style.width = `${this.px2percent(newWidth, parentWidth)}%`;
const adjustment = oldWidth - newWidth;
// Calculate new compensate width as the original width + adjustment.
// Do not allow to set it below minCompensateWidth.
const newCompensateWidth =
Math.max(adjustment + oldCompensateWidth, minCompensateWidth);
compensateHeaderRef.style.width =
`${this.px2percent(newCompensateWidth, parentWidth)}%`;
// Do not allow to reset size of column when compensate column is at minWidth.
if (newCompensateWidth === minCompensateWidth) {
headerRef.style.width =
`${this.px2percent((sumOfBothColumns - newCompensateWidth), parentWidth)}%`;
}
}
/**
* After resizing - we get the width for each 'column'
* and convert it into % and store it in user prefs.
* Also resets the 'resizing' cursor back to initial.
*/
onStopMove() {
this.updateColumnsWidth();
// If waterfall is visible and width has changed, call resizeWaterfall.
const waterfallRef = this.refs.waterfallHeader;
if (waterfallRef) {
const { waterfallWidth } = this.props;
const realWaterfallWidth = waterfallRef.getBoundingClientRect().width;
if (Math.round(waterfallWidth) !== Math.round(realWaterfallWidth)) {
this.resizeWaterfall();
}
}
// Restore cursor back to default.
const container = document.querySelector(".request-list-container");
container.style.cursor = "initial";
this.requestListHeader.classList.remove("dragging");
}
/**
* Helper method to get the name of the column that will compensate
* the width change. It should be the last column before waterfall,
* (if waterfall visible) otherwise it is simply the last visible column.
*/
getCompensateHeader() {
const visibleColumns = this.getVisibleColumns();
const lastColumn = visibleColumns[visibleColumns.length - 1].name;
const delta = (lastColumn === "waterfall") ? 2 : 1;
return visibleColumns[visibleColumns.length - delta].name;
}
/**
* Called from onMove() when resizing waterfall column
* because waterfall is a special case, where ALL other
* columns are made smaller when waterfall is bigger and vice versa.
*/
moveWaterfall(x, parentWidth) {
const visibleColumns = this.getVisibleColumns();
const minWaterfall = this.getMinWidth("waterfall");
const waterfallRef = this.refs.waterfallHeader;
// Compute and set style.width for waterfall.
const waterfallRefRect = waterfallRef.getBoundingClientRect();
const oldWidth = waterfallRefRect.width;
const adjustment = waterfallRefRect.left - x;
if (this.allColumnsAtMinWidth() && adjustment > 0) {
// When we want to make waterfall wider but all
// other columns are already at minWidth => return.
return;
}
const newWidth = Math.max(oldWidth + adjustment, minWaterfall);
// Now distribute evenly the change in width to all other columns except waterfall.
const changeInWidth = oldWidth - newWidth;
const widths = this.autoSizeWidths(changeInWidth, visibleColumns);
// Set the new computed width for waterfall into array widths.
widths[widths.length - 1] = newWidth;
// Update style for all columns from array widths.
let i = 0;
visibleColumns.forEach(col => {
const name = col.name;
const headerRef = this.refs[`${name}Header`];
headerRef.style.width = `${this.px2percent(widths[i], parentWidth)}%`;
i++;
});
}
/**
* Helper method that checks if all columns have reached their minWidth.
* This can happen when making waterfall column wider.
*/
allColumnsAtMinWidth() {
const visibleColumns = this.getVisibleColumns();
// Do not check width for waterfall because
// when all are getting smaller, waterfall is getting bigger.
for (let i = 0; i < visibleColumns.length - 1; i++) {
const name = visibleColumns[i].name;
const headerRef = this.refs[`${name}Header`];
const minColWidth = this.getMinWidth(name);
if (headerRef.getBoundingClientRect().width > minColWidth) {
return false;
}
}
return true;
}
/**
* Method takes the total change in width for waterfall column
* and distributes it among all other columns. Returns an array
* where all visible columns have newly computed width in pixels.
*/
autoSizeWidths(changeInWidth, visibleColumns) {
const widths = visibleColumns.map(col => {
const headerRef = this.refs[`${col.name}Header`];
const colWidth = headerRef.getBoundingClientRect().width;
return colWidth;
});
// Divide changeInWidth among all columns but waterfall (that's why -1).
const changeInWidthPerColumn = changeInWidth / (widths.length - 1);
while (changeInWidth) {
const lastChangeInWidth = changeInWidth;
// In the loop adjust all columns except last one - waterfall
for (let i = 0; i < widths.length - 1; i++) {
const name = visibleColumns[i].name;
const minColWidth = this.getMinWidth(name);
const newColWidth = Math.max(widths[i] + changeInWidthPerColumn, minColWidth);
widths[i] = newColWidth;
if (changeInWidth > 0) {
changeInWidth -= (newColWidth - widths[i]);
} else {
changeInWidth += (newColWidth - widths[i]);
}
if (!changeInWidth) {
break;
}
}
if (lastChangeInWidth == changeInWidth) {
break;
}
}
return widths;
}
/**
* Method returns 'true' - if the column widths need to be updated
* when the total % is less or more than 100%.
* It returns 'false' if they add up to 100% => no need to update.
*/
shouldUpdateWidths() {
const visibleColumns = this.getVisibleColumns();
let totalPercent = 0;
visibleColumns.forEach(col => {
const name = col.name;
const headerRef = this.refs[`${name}Header`];
// Get column width from style.
let widthFromStyle = 0;
// In case the column is in visibleColumns but has display:none
// we don't want to count its style.width into totalPercent.
if (headerRef.getBoundingClientRect().width > 0) {
widthFromStyle = headerRef.style.width.slice(0, -1);
}
totalPercent += +widthFromStyle; // + converts it to a number
});
// Do not update if total percent is from 99-101% or when it is 0
// - it means that no columns are displayed (e.g. other panel is currently selected).
return Math.round(totalPercent) !== 100 && totalPercent !== 0;
}
/**
* Method reads real width of each column header
* and updates the style.width for that header.
* It returns updated columnsData.
*/
updateColumnsWidth() {
const visibleColumns = this.getVisibleColumns();
const parentEl = document.querySelector(".requests-list-headers");
const parentElRect = parentEl.getBoundingClientRect();
const parentWidth = parentElRect.width;
const newWidths = [];
visibleColumns.forEach(col => {
const name = col.name;
const headerRef = this.refs[`${name}Header`];
const headerWidth = headerRef.getBoundingClientRect().width;
// Get actual column width, change into %, update style
const width = this.px2percent(headerWidth, parentWidth);
if (width > 0) {
// This prevents saving width 0 for waterfall when it is not showing for
// @media (max-width: 700px)
newWidths.push({name, width});
}
});
this.props.setColumnsWidth(newWidths);
}
/**
* Helper method to convert pixels into percent based on parent container width
*/
px2percent(pxWidth, parentWidth) {
const percent = Math.round((100 * pxWidth / parentWidth) * 100) / 100;
return percent;
}
/**
* Helper method to get visibleColumns;
*/
getVisibleColumns() {
const { columns } = this.props;
return HEADERS.filter((header) => columns[header.name]);
}
/**
* Helper method to get minWidth from columnsData;
*/
getMinWidth(colName) {
const columnsData = this.props.columnsData;
if (columnsData.has(colName)) {
return columnsData.get(colName).minWidth;
}
return MIN_COLUMN_WIDTH;
}
/**
* Render one column header from the table headers.
*/
renderColumn(header) {
const columnsData = this.props.columnsData;
const visibleColumns = this.getVisibleColumns();
const lastVisibleColumn = visibleColumns[visibleColumns.length - 1].name;
const name = header.name;
const boxName = header.boxName || name;
const label = header.noLocalization
? name : L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
const { scale, sort, sortBy, waterfallWidth } = this.props;
let sorted, sortedTitle;
const active = sort.type == name ? true : undefined;
if (active) {
sorted = sort.ascending ? "ascending" : "descending";
sortedTitle = L10N.getStr(sort.ascending
? "networkMenu.sortedAsc"
: "networkMenu.sortedDesc");
}
// If the pref for this column width exists, set the style
// otherwise use default.
let colWidth = DEFAULT_COLUMN_WIDTH;
if (columnsData.has(name)) {
const oneColumnEl = columnsData.get(name);
colWidth = oneColumnEl.width;
}
const columnStyle = {
width: colWidth + "%",
};
// Support for columns resizing is currently hidden behind a pref.
const draggable = RESIZE_COLUMNS ? Draggable({
className: "column-resizer ",
onStart: () => this.onStartMove(),
onStop: () => this.onStopMove(),
onMove: (x) => this.onMove(name, x),
}) : undefined;
return (
dom.td({
id: `requests-list-${boxName}-header-box`,
className: `requests-list-column requests-list-${boxName}`,
style: columnStyle,
key: name,
ref: `${name}Header`,
// Used to style the next column.
"data-active": active,
},
button({
id: `requests-list-${name}-button`,
className: `requests-list-header-button`,
"data-sorted": sorted,
title: sortedTitle ? `${label} (${sortedTitle})` : label,
onClick: () => sortBy(name),
},
name === "waterfall"
? this.waterfallLabel(waterfallWidth, scale, label)
: div({ className: "button-text" }, label),
div({ className: "button-icon" })
),
(name !== lastVisibleColumn) && draggable
)
);
}
/**
* Render all columns in the table header
*/
renderColumns() {
const visibleColumns = this.getVisibleColumns();
return visibleColumns.map(header => this.renderColumn(header));
}
render() {
return (
dom.thead({ className: "devtools-toolbar requests-list-headers-group" },
dom.tr({
className: "requests-list-headers",
onContextMenu: this.onContextMenu,
ref: node => {
this.requestListHeader = node;
},
},
HEADERS.filter((header) => columns[header.name]).map((header) => {
const name = header.name;
const boxName = header.boxName || name;
const label = header.noLocalization
? name : L10N.getStr(`netmonitor.toolbar.${header.label || name}`);
let sorted, sortedTitle;
const active = sort.type == name ? true : undefined;
if (active) {
sorted = sort.ascending ? "ascending" : "descending";
sortedTitle = L10N.getStr(sort.ascending
? "networkMenu.sortedAsc"
: "networkMenu.sortedDesc");
}
return (
dom.td({
id: `requests-list-${boxName}-header-box`,
className: `requests-list-column requests-list-${boxName}`,
key: name,
ref: `${name}Header`,
// Used to style the next column.
"data-active": active,
},
button({
id: `requests-list-${name}-button`,
className: `requests-list-header-button`,
"data-sorted": sorted,
title: sortedTitle ? `${label} (${sortedTitle})` : label,
onClick: () => sortBy(name),
},
name === "waterfall"
? this.waterfallLabel(waterfallWidth, scale, label)
: div({ className: "button-text" }, label),
div({ className: "button-icon" })
)
)
);
})
this.renderColumns(),
)
)
);
@ -220,6 +570,7 @@ class RequestListHeader extends Component {
module.exports = connect(
(state) => ({
columns: state.ui.columns,
columnsData: state.ui.columnsData,
firstRequestStartedMillis: state.requests.firstStartedMillis,
scale: getWaterfallScale(state),
sort: state.sort,
@ -231,5 +582,6 @@ module.exports = connect(
resizeWaterfall: (width) => dispatch(Actions.resizeWaterfall(width)),
sortBy: (type) => dispatch(Actions.sortBy(type)),
toggleColumn: (column) => dispatch(Actions.toggleColumn(column)),
setColumnsWidth: (widths) => dispatch(Actions.setColumnsWidth(widths)),
})
)(RequestListHeader);

Просмотреть файл

@ -30,6 +30,7 @@ const actionTypes = {
TOGGLE_REQUEST_FILTER_TYPE: "TOGGLE_REQUEST_FILTER_TYPE",
UPDATE_REQUEST: "UPDATE_REQUEST",
WATERFALL_RESIZE: "WATERFALL_RESIZE",
SET_COLUMNS_WIDTH: "SET_COLUMNS_WIDTH",
};
// Descriptions for what this frontend is currently doing.
@ -332,6 +333,11 @@ const TIMING_KEYS = [
"receive",
];
// Minimal width of Network Monitor column is 30px, for Waterfall 150px
// Default width of columns (which are not defined in DEFAULT_COLUMNS_DATA) is 8%
const MIN_COLUMN_WIDTH = 30; // in px
const DEFAULT_COLUMN_WIDTH = 8; // in %
const general = {
ACTIVITY_TYPE,
EVENTS,
@ -344,6 +350,8 @@ const general = {
REQUESTS_WATERFALL,
PANELS,
TIMING_KEYS,
MIN_COLUMN_WIDTH,
DEFAULT_COLUMN_WIDTH,
};
// flatten constants

Просмотреть файл

@ -7,6 +7,11 @@
const Services = require("Services");
const { applyMiddleware, createStore } = require("devtools/client/shared/vendor/redux");
const {
MIN_COLUMN_WIDTH,
DEFAULT_COLUMN_WIDTH,
} = require("./constants");
// Middleware
const batching = require("./middleware/batching");
const prefs = require("./middleware/prefs");
@ -21,7 +26,7 @@ const { FilterTypes, Filters } = require("./reducers/filters");
const { Requests } = require("./reducers/requests");
const { Sort } = require("./reducers/sort");
const { TimingMarkers } = require("./reducers/timing-markers");
const { UI, Columns } = require("./reducers/ui");
const { UI, Columns, ColumnsData } = require("./reducers/ui");
/**
* Configure state and middleware for the Network monitor tool.
@ -37,6 +42,7 @@ function configureStore(connector, telemetry) {
timingMarkers: new TimingMarkers(),
ui: UI({
columns: getColumnState(),
columnsData: getColumnsData(),
}),
};
@ -70,6 +76,27 @@ function getColumnState() {
return state;
}
/**
* Get columns data (width, min-width)
*/
function getColumnsData() {
const columnsData = getPref("devtools.netmonitor.columnsData");
if (!columnsData.length) {
return ColumnsData();
}
const newMap = new Map();
columnsData.forEach(col => {
if (col.name) {
col.minWidth = col.minWidth ? col.minWidth : MIN_COLUMN_WIDTH;
col.width = col.width ? col.width : DEFAULT_COLUMN_WIDTH;
newMap.set(col.name, col);
}
});
return newMap;
}
/**
* Get filter state from preferences.
*/

Просмотреть файл

@ -12,6 +12,7 @@ const {
TOGGLE_REQUEST_FILTER_TYPE,
ENABLE_PERSISTENT_LOGS,
DISABLE_BROWSER_CACHE,
SET_COLUMNS_WIDTH,
} = require("../constants");
/**
@ -40,20 +41,45 @@ function prefsMiddleware(store) {
"devtools.cache.disabled", store.getState().ui.browserCacheDisabled);
break;
case TOGGLE_COLUMN:
persistVisibleColumns(store.getState());
break;
case RESET_COLUMNS:
const visibleColumns = [];
const columns = store.getState().ui.columns;
for (const column in columns) {
if (columns[column]) {
visibleColumns.push(column);
}
}
Services.prefs.setCharPref(
"devtools.netmonitor.visibleColumns", JSON.stringify(visibleColumns));
persistVisibleColumns(store.getState());
persistColumnsData(store.getState());
break;
case SET_COLUMNS_WIDTH:
persistColumnsData(store.getState());
break;
}
return res;
};
}
/**
* Store list of visible columns into preferences.
*/
function persistVisibleColumns(state) {
const visibleColumns = [];
const columns = state.ui.columns;
for (const column in columns) {
if (columns[column]) {
visibleColumns.push(column);
}
}
Services.prefs.setCharPref(
"devtools.netmonitor.visibleColumns",
JSON.stringify(visibleColumns));
}
/**
* Store columns data (width, min-width, etc.) into preferences.
*/
function persistColumnsData(state) {
const columnsData = [...state.ui.columnsData.values()];
Services.prefs.setCharPref(
"devtools.netmonitor.columnsData",
JSON.stringify(columnsData));
}
module.exports = prefsMiddleware;

Просмотреть файл

@ -21,6 +21,8 @@ const {
TOGGLE_COLUMN,
WATERFALL_RESIZE,
PANELS,
MIN_COLUMN_WIDTH,
SET_COLUMNS_WIDTH,
} = require("../constants");
const cols = {
@ -44,6 +46,7 @@ const cols = {
latency: false,
waterfall: true,
};
function Columns() {
return Object.assign(
cols,
@ -51,9 +54,17 @@ function Columns() {
);
}
function ColumnsData() {
const defaultColumnsData = JSON.parse(
Services.prefs.getDefaultBranch(null).getCharPref("devtools.netmonitor.columnsData")
);
return new Map(defaultColumnsData.map(i => [i.name, i]));
}
function UI(initialState = {}) {
return {
columns: Columns(),
columnsData: ColumnsData(),
detailsPanelSelectedTab: PANELS.HEADERS,
networkDetailsOpen: false,
networkDetailsWidth: null,
@ -70,6 +81,7 @@ function resetColumns(state) {
return {
...state,
columns: Columns(),
columnsData: ColumnsData(),
};
}
@ -139,6 +151,30 @@ function toggleColumn(state, action) {
};
}
function setColumnsWidth(state, action) {
const { widths } = action;
const columnsData = new Map(state.columnsData);
widths.forEach(col => {
let data = columnsData.get(col.name);
if (!data) {
data = {
name: col.name,
minWidth: MIN_COLUMN_WIDTH,
};
}
columnsData.set(col.name, {
...data,
width: col.width,
});
});
return {
...state,
columnsData: columnsData,
};
}
function ui(state = UI(), action) {
switch (action.type) {
case CLEAR_REQUESTS:
@ -167,6 +203,8 @@ function ui(state = UI(), action) {
return toggleColumn(state, action);
case WATERFALL_RESIZE:
return resizeWaterfall(state, action);
case SET_COLUMNS_WIDTH:
return setColumnsWidth(state, action);
default:
return state;
}
@ -174,6 +212,7 @@ function ui(state = UI(), action) {
module.exports = {
Columns,
ColumnsData,
UI,
ui,
};

Просмотреть файл

@ -13,5 +13,6 @@ exports.Prefs = new PrefsHelper("devtools.netmonitor", {
networkDetailsWidth: ["Int", "panes-network-details-width"],
networkDetailsHeight: ["Int", "panes-network-details-height"],
visibleColumns: ["Json", "visibleColumns"],
columnsData: ["Json", "columnsData"],
filters: ["Json", "filters"],
});

Просмотреть файл

@ -135,6 +135,7 @@ skip-if = (os == 'mac') || (os == 'win' && os_version == '10.0') # Bug 1479782
[browser_net_headers-alignment.js]
[browser_net_headers_filter.js]
[browser_net_headers_sorted.js]
[browser_net_headers-resize.js]
[browser_net_image-tooltip.js]
[browser_net_json-b64.js]
[browser_net_json-empty.js]

Просмотреть файл

@ -4,14 +4,21 @@
"use strict";
/**
* Tests that last visible column can't be hidden
* Tests that last visible column can't be hidden. Note that the column
* header is visible only if there are requests in the list.
*/
add_task(async function() {
const { monitor } = await initNetMonitor(SIMPLE_URL);
const { monitor, tab } = await initNetMonitor(SIMPLE_URL);
info("Starting test... ");
const { document, store, parent } = monitor.panelWin;
const { document, store, parent, windowRequire } = monitor.panelWin;
const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
const wait = waitForNetworkEvents(monitor, 1);
tab.linkedBrowser.reload();
await wait;
const initialColumns = store.getState().ui.columns;
for (const column in initialColumns) {

Просмотреть файл

@ -4,17 +4,24 @@
"use strict";
/**
* Tests if visible columns are properly saved
* Tests if visible columns are properly saved. Note that the column
* header is visible only if there are requests in the list.
*/
add_task(async function() {
Services.prefs.setCharPref("devtools.netmonitor.visibleColumns",
'["status", "contentSize", "waterfall"]');
const { monitor } = await initNetMonitor(SIMPLE_URL);
const { monitor, tab } = await initNetMonitor(SIMPLE_URL);
info("Starting test... ");
const { document } = monitor.panelWin;
const { document, store, windowRequire } = monitor.panelWin;
const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
const wait = waitForNetworkEvents(monitor, 1);
tab.linkedBrowser.reload();
await wait;
ok(document.querySelector("#requests-list-status-button"),
"Status column should be shown");

Просмотреть файл

@ -4,16 +4,23 @@
"use strict";
/**
* Tests reset column menu item
* Tests reset column menu item. Note that the column
* header is visible only if there are requests in the list.
*/
add_task(async function() {
const { monitor } = await initNetMonitor(SIMPLE_URL);
const { monitor, tab } = await initNetMonitor(SIMPLE_URL);
info("Starting test... ");
const { document, parent, windowRequire } = monitor.panelWin;
const { document, store, parent, windowRequire } = monitor.panelWin;
const { Prefs } = windowRequire("devtools/client/netmonitor/src/utils/prefs");
const prefBefore = Prefs.visibleColumns;
const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
const wait = waitForNetworkEvents(monitor, 1);
tab.linkedBrowser.reload();
await wait;
await hideColumn(monitor, "status");
await hideColumn(monitor, "waterfall");

Просмотреть файл

@ -4,7 +4,8 @@
"use strict";
/**
* Tests for timings columns.
* Tests for timings columns. Note that the column
* header is visible only if there are requests in the list.
*/
add_task(async function() {
const { tab, monitor } = await initNetMonitor(SIMPLE_URL);
@ -16,6 +17,10 @@ add_task(async function() {
const visibleColumns = store.getState().ui.columns;
const wait = waitForNetworkEvents(monitor, 1);
tab.linkedBrowser.reload();
await wait;
// Hide the waterfall column to make sure timing data are fetched
// by the other timing columns ("endTime", "responseTime", "duration",
// "latency").

Просмотреть файл

@ -0,0 +1,199 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests resizing of columns in NetMonitor.
*/
add_task(async function() {
// Reset visibleColumns so we only get the default ones
// and not all that are set in head.js
Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns");
let visibleColumns = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.visibleColumns")
);
// Init network monitor
const { tab, monitor } = await initNetMonitor(SIMPLE_URL);
info("Starting test... ");
const { document, windowRequire, store } = monitor.panelWin;
const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
store.dispatch(Actions.batchEnable(false));
// Wait for network events (to have some requests in the table)
const wait = waitForNetworkEvents(monitor, 1);
tab.linkedBrowser.reload();
await wait;
const headers = document.querySelector(".requests-list-headers");
const parentWidth = headers.getBoundingClientRect().width;
// 1. Change File column from 25% (default) to 20%
// Size column should then change from 5% (default) to 10%
// When File width changes, contentSize should compensate the change.
info("Resize file & check changed prefs...");
const fileHeader = document.querySelector(`#requests-list-file-header-box`);
resizeColumn(fileHeader, 20, parentWidth);
// after resize - get fresh prefs for tests
let columnsData = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.columnsData")
);
checkColumnsData(columnsData, "file", 20);
checkColumnsData(columnsData, "contentSize", 10);
checkSumOfVisibleColumns(columnsData, visibleColumns);
// 2. Change Waterfall column width and check that the size
// of waterfall changed correctly and all the other columns changed size.
info("Resize waterfall & check changed prefs...");
const waterfallHeader = document.querySelector(`#requests-list-waterfall-header-box`);
// before resizing waterfall -> save old columnsData for later testing
const oldColumnsData = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.columnsData")
);
resizeWaterfallColumn(waterfallHeader, 30, parentWidth); // 30 fails currently!
// after resize - get fresh prefs for tests
columnsData = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.columnsData")
);
checkColumnsData(columnsData, "waterfall", 30);
checkSumOfVisibleColumns(columnsData, visibleColumns);
checkAllColumnsChanged(columnsData, oldColumnsData, visibleColumns);
// 3. Check that all rows have the right column sizes.
info("Checking alignment of columns and headers...");
const requestsContainer = document.querySelector(".requests-list-row-group");
testColumnsAlignment(headers, requestsContainer);
// 4. Hide all columns but size and waterfall
// and check that they resize correctly. Then resize
// waterfall to 50% => size should take up 50%
info("Hide all but 2 columns - size & waterfall and check resizing...");
await hideMoreColumns(monitor,
["status", "method", "domain", "file", "cause", "type", "transferred"]);
resizeWaterfallColumn(waterfallHeader, 50, parentWidth);
// after resize - get fresh prefs for tests
columnsData = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.columnsData")
);
visibleColumns = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.visibleColumns")
);
checkColumnsData(columnsData, "contentSize", 50);
checkColumnsData(columnsData, "waterfall", 50);
checkSumOfVisibleColumns(columnsData, visibleColumns);
// 5. Hide all columns but domain and file
// and resize domain to 50% => file should be 50%
info("Hide all but 2 columns - domain & file and check resizing...");
await showMoreColumns(monitor, ["domain", "file"]);
await hideMoreColumns(monitor, ["contentSize", "waterfall"]);
const domainHeader = document.querySelector(`#requests-list-domain-header-box`);
resizeColumn(domainHeader, 50, parentWidth);
// after resize - get fresh prefs for tests
columnsData = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.columnsData")
);
visibleColumns = JSON.parse(
Services.prefs.getCharPref("devtools.netmonitor.visibleColumns")
);
checkColumnsData(columnsData, "domain", 50);
checkColumnsData(columnsData, "file", 50);
checkSumOfVisibleColumns(columnsData, visibleColumns);
// Done: clean up.
return teardown(monitor);
});
async function hideMoreColumns(monitor, arr) {
for (let i = 0; i < arr.length; i++) {
await hideColumn(monitor, arr[i]);
}
}
async function showMoreColumns(monitor, arr) {
for (let i = 0; i < arr.length; i++) {
await showColumn(monitor, arr[i]);
}
}
function resizeColumn(columnHeader, newPercent, parentWidth) {
const newWidthInPixels = newPercent * parentWidth / 100;
const win = columnHeader.ownerDocument.defaultView;
const mouseDown = columnHeader.getBoundingClientRect().width;
const mouseMove = newWidthInPixels;
EventUtils.synthesizeMouse(columnHeader, mouseDown, 1, { type: "mousedown" }, win);
EventUtils.synthesizeMouse(columnHeader, mouseMove, 1, { type: "mousemove" }, win);
EventUtils.synthesizeMouse(columnHeader, mouseMove, 1, { type: "mouseup" }, win);
}
function resizeWaterfallColumn(columnHeader, newPercent, parentWidth) {
const newWidthInPixels = newPercent * parentWidth / 100;
const win = columnHeader.ownerDocument.defaultView;
const mouseDown = columnHeader.getBoundingClientRect().left;
const mouseMove =
mouseDown + (columnHeader.getBoundingClientRect().width - newWidthInPixels);
EventUtils.synthesizeMouse(
columnHeader.parentElement, mouseDown, 1, { type: "mousedown" }, win);
EventUtils.synthesizeMouse(
columnHeader.parentElement, mouseMove, 1, { type: "mousemove" }, win);
EventUtils.synthesizeMouse(
columnHeader.parentElement, mouseMove, 1, { type: "mouseup" }, win);
}
function checkColumnsData(columnsData, column, expectedWidth) {
const widthInPref = Math.round(getWidthFromPref(columnsData, column));
is(widthInPref, expectedWidth, "Column " + column + " has expected size.");
}
function checkSumOfVisibleColumns(columnsData, visibleColumns) {
let sum = 0;
visibleColumns.forEach(column => {
sum += getWidthFromPref(columnsData, column);
});
sum = Math.round(sum);
is(sum, 100, "All visible columns cover 100%.");
}
function getWidthFromPref(columnsData, column) {
const widthInPref = columnsData.find(function(element) {
return element.name === column;
}).width;
return widthInPref;
}
function checkAllColumnsChanged(columnsData, oldColumnsData, visibleColumns) {
const oldWaterfallWidth = getWidthFromPref(oldColumnsData, "waterfall");
const newWaterfallWidth = getWidthFromPref(columnsData, "waterfall");
visibleColumns.forEach(column => {
// do not test waterfall against waterfall
if (column !== "waterfall") {
const oldWidth = getWidthFromPref(oldColumnsData, column);
const newWidth = getWidthFromPref(columnsData, column);
// Test that if waterfall is smaller all other columns are bigger
if (oldWaterfallWidth > newWaterfallWidth) {
is(oldWidth < newWidth, true,
"Column " + column + " has changed width correctly.");
}
// Test that if waterfall is bigger all other columns are smaller
if (oldWaterfallWidth < newWaterfallWidth) {
is(oldWidth > newWidth, true,
"Column " + column + " has changed width correctly.");
}
}
});
}

Просмотреть файл

@ -118,16 +118,34 @@ Services.prefs.setCharPref(
"\"startTime\",\"status\",\"transferred\",\"type\",\"waterfall\"]"
);
Services.prefs.setCharPref("devtools.netmonitor.columnsData",
'[{"name":"status","minWidth":30,"width":5},' +
'{"name":"method","minWidth":30,"width":5},' +
'{"name":"domain","minWidth":30,"width":10},' +
'{"name":"file","minWidth":30,"width":25},' +
'{"name":"cause","minWidth":30,"width":10},' +
'{"name":"type","minWidth":30,"width":5},' +
'{"name":"transferred","minWidth":30,"width":10},' +
'{"name":"contentSize","minWidth":30,"width":5},' +
'{"name":"waterfall","minWidth":150,"width":25}]');
// Increase UI limit for responses rendered using CodeMirror in tests.
Services.prefs.setIntPref("devtools.netmonitor.response.ui.limit", 1024 * 105);
// Support for columns resizing is currently hidden behind this pref,
// but testing is on
Services.prefs.setBoolPref("devtools.netmonitor.features.resizeColumns", true);
registerCleanupFunction(() => {
info("finish() was called, cleaning up...");
Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
Services.prefs.setCharPref("devtools.netmonitor.filters", gDefaultFilters);
Services.prefs.clearUserPref("devtools.cache.disabled");
Services.prefs.clearUserPref("devtools.netmonitor.columnsData");
Services.prefs.clearUserPref("devtools.netmonitor.response.ui.limit");
Services.prefs.clearUserPref("devtools.netmonitor.visibleColumns");
Services.prefs.clearUserPref("devtools.netmonitor.features.resizeColumns");
Services.cookies.removeAll();
});

Просмотреть файл

@ -175,6 +175,12 @@ pref("devtools.netmonitor.filters", "[\"all\"]");
pref("devtools.netmonitor.visibleColumns",
"[\"status\",\"method\",\"domain\",\"file\",\"cause\",\"type\",\"transferred\",\"contentSize\",\"waterfall\"]"
);
pref("devtools.netmonitor.columnsData",
'[{"name":"status","minWidth":30,"width":5}, {"name":"method","minWidth":30,"width":5}, {"name":"domain","minWidth":30,"width":10}, {"name":"file","minWidth":30,"width":25}, {"name":"cause","minWidth":30,"width":10},{"name":"type","minWidth":30,"width":5},{"name":"transferred","minWidth":30,"width":10},{"name":"contentSize","minWidth":30,"width":5},{"name":"waterfall","minWidth":150,"width":25}]');
// Support for columns resizing is currently hidden behind this pref.
pref("devtools.netmonitor.features.resizeColumns", false);
pref("devtools.netmonitor.response.ui.limit", 10240);
// Save request/response bodies yes/no.