├── .gitignore
├── LICENSE
├── README.md
├── package.json
├── src
├── app.jsx
├── app.scss
├── components
│ ├── FilterBar.jsx
│ ├── HarViewer.jsx
│ ├── SampleSelector.jsx
│ ├── file-type
│ │ ├── FileType.jsx
│ │ └── _file-type.scss
│ ├── har-entry-table
│ │ ├── HarEntryTable.jsx
│ │ └── _har-entry-table.scss
│ ├── pie-chart
│ │ ├── ChartBuilder.js
│ │ └── TypePieChart.jsx
│ └── timebar
│ │ ├── TimeBar.jsx
│ │ ├── TimingDetails.jsx
│ │ └── _timebar.scss
├── core
│ ├── Entry.js
│ ├── Page.js
│ ├── formatter.js
│ ├── har-parser.js
│ └── mime-types.js
├── samples.js
└── store
│ ├── HarActions.js
│ ├── HarStore.js
│ ├── alt.js
│ └── sample-hars
│ ├── facebook.github.io.json
│ ├── stackoverflow.com.json
│ └── www.nytimes.com.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | .idea
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Tuts+
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [React Deep Dive: Build a React App With Webpack][published url]
2 | ## Instructor: [Pavan Podila][instructor url]
3 |
4 |
5 | React is a library for building user interfaces that has taken the web development world by storm. With an innovative model for efficient rendering to a "virtual DOM", an ecosystem of thousands of related modules on NPM, and a complex toolchain, React is a powerful and subtle technology. But this means it can sometimes be hard to get up to speed for full-scale app development.
6 |
7 | In this course, Envato Tuts+ instructor Pavan Podila will take you on a deep dive into ReactJS by building a complex app that makes use of libraries such as D3, React-Bootstrap, and FixedDataTable. You'll learn how to use tools such as Webpack that are necessary to build any full-fledged React app. You'll also see how to write React apps in ES6 (ECMAScript 2015) for a cleaner and more elegant syntax.
8 |
9 | The course project will be to build a HAR (Http ARchive) file viewer. HAR files are a representation the network traffic in a web browser. Chrome has a built-in support in the Network panel of DevTools. You will build your own version of this functionality with a custom HAR viewer.
10 |
11 | By the end of this course, you will have a stronger grasp on starting your own React projects, organizing your code using React components, and bundling your app code with Webpack. You'll also learn a professional dev workflow for React. You'll walk away with a stronger understanding of React components and their lifecycle by learning how to wrap existing 3rd-party libraries within React components.
12 |
13 |
14 | ## Source Files Description
15 |
16 |
17 | All of the source code is inside the `/src` folder. Inside you will find the `components` folder that contains all the React Components. `core` and `store` folders contain the supporting code for the components.
18 |
19 | You will also see lesson checkpoints as a separate branch in the repo.
20 |
21 | ------
22 |
23 | These are source files for the Tuts+ course: [React Deep Dive: Build a React App With Webpack][published url]
24 |
25 | Available on [Tuts+](https://tutsplus.com). Teaching skills to millions worldwide.
26 |
27 | [published url]: https://code.tutsplus.com/courses/react-deep-dive-build-a-react-app-with-webpack
28 | [instructor url]: https://tutsplus.com/authors/pavan-podila
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-deep-dive",
3 | "version": "0.0.1",
4 | "devDependencies": {
5 | "babel-core": "^5.6.15",
6 | "babel-loader": "^5.3.1",
7 | "babel-runtime": "^5.6.15",
8 | "css-loader": "^0.15.2",
9 | "file-loader": "^0.8.4",
10 | "html-webpack-plugin": "^2.20.0",
11 | "local-web-server": "^0.5.21",
12 | "node-libs-browser": "^0.5.2",
13 | "node-sass": "^3.2.0",
14 | "sass-loader": "^1.0.2",
15 | "style-loader": "^0.12.3",
16 | "url-loader": "^0.5.6",
17 | "webpack": "^1.10.1",
18 | "webpack-dev-server": "^1.10.1"
19 | },
20 | "scripts": {
21 | "build": "webpack",
22 | "dev": "webpack-dev-server --inline --colors --progress",
23 | "server": "ws -p 8081 -d ./src/store"
24 | },
25 | "dependencies": {
26 | "alt": "^0.17.1",
27 | "bootstrap": "^3.3.5",
28 | "d3": "^3.5.6",
29 | "fixed-data-table": "^0.4.1",
30 | "json-loader": "^0.5.2",
31 | "lodash": "^3.10.0",
32 | "react": "^0.13.3",
33 | "react-bootstrap": "^0.23.7"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app.jsx:
--------------------------------------------------------------------------------
1 | require('bootstrap/dist/css/bootstrap.css');
2 | require('bootstrap/dist/css/bootstrap-theme.css');
3 |
4 | require('./app.scss');
5 |
6 | import HarViewer from './components/HarViewer.jsx';
7 | import React from 'react';
8 |
9 | React.render(
10 | ,
11 | document.body
12 | );
13 |
--------------------------------------------------------------------------------
/src/app.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100vh;
4 | overflow-x: hidden;
5 | }
6 |
7 | .row {
8 | min-width: 770px;
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/components/FilterBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Button, ButtonGroup, Input, Row, Col} from 'react-bootstrap';
3 | import _ from 'lodash';
4 |
5 | const PropTypes = React.PropTypes;
6 | import mimeTypes from '../core/mime-types.js';
7 |
8 | export default class FilterBar extends React.Component {
9 |
10 | constructor() {
11 | super();
12 | this.state = {
13 | type: 'all'
14 | };
15 | }
16 |
17 | render() {
18 | var buttons = _.map(_.keys(mimeTypes.types), (x)=> {
19 | return this._createButton(x, mimeTypes.types[x].label);
20 | });
21 | return (
22 |
23 |
24 |
25 | {this._createButton('all', 'All')}
26 | {buttons}
27 |
28 |
29 |
30 |
35 |
36 |
37 | );
38 | }
39 |
40 | _filterTextChanged() {
41 | if (this.props.onFilterTextChange) {
42 | this.props.onFilterTextChange(this.refs.filterText.getValue());
43 | }
44 | }
45 |
46 | _createButton(type, label) {
47 | var handler = this._filterRequested.bind(this, type);
48 | return (
49 | {label}
53 |
54 | );
55 | }
56 |
57 | _filterRequested(type, event) {
58 | this.setState({type: type});
59 | if (this.props.onChange) {
60 | this.props.onChange(type);
61 | }
62 | }
63 | };
64 |
65 | FilterBar.defaultProps = {
66 | onChange: null,
67 | onFilterTextChange: null
68 | };
69 |
70 | FilterBar.propTypes = {
71 | onChange: PropTypes.func,
72 | onFilterTextChange: PropTypes.func
73 | };
--------------------------------------------------------------------------------
/src/components/HarViewer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Grid, Row, Col, PageHeader, Alert} from 'react-bootstrap';
3 | import _ from 'lodash';
4 | import d3 from 'd3';
5 |
6 | import harParser from '../core/har-parser.js'
7 |
8 | import HarEntryTable from './har-entry-table/HarEntryTable.jsx';
9 | import FilterBar from './FilterBar.jsx';
10 | import TypePieChart from './pie-chart/TypePieChart.jsx';
11 | import SampleSelector from './SampleSelector.jsx';
12 |
13 | import HarActions from '../store/HarActions';
14 | import HarStore from '../store/HarStore';
15 |
16 | export default class HarViewer extends React.Component {
17 |
18 | constructor() {
19 | super();
20 | this.state = {
21 | activeHar: HarStore.getState().activeHar,
22 | filterType: 'all',
23 | filterText: '',
24 | sortKey: null,
25 | sortDirection: null
26 | };
27 | }
28 |
29 | render() {
30 | "use strict";
31 |
32 | var content = this.state.activeHar
33 | ? this._renderViewer(this.state.activeHar)
34 | : this._renderEmptyViewer();
35 |
36 | return (
37 |
38 | {this._renderHeader()}
39 | {content}
40 |
41 | );
42 | }
43 |
44 | _renderEmptyViewer() {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 | No HAR loaded
52 |
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | _renderHeader() {
60 | return (
61 |
62 |
63 |
64 | Har Viewer
65 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | _renderViewer(har) {
75 | var pages = harParser.parse(har),
76 | currentPage = pages[0],
77 | timeScale = this._prepareScale(currentPage.entries, currentPage),
78 | filter = {
79 | type: this.state.filterType,
80 | text: this.state.filterText
81 | },
82 | filteredEntries = this._filterEntries(filter, currentPage.entries),
83 | sortedEntries = this._sortEntriesByKey(this.state.sortKey, this.state.sortDirection, filteredEntries);
84 |
85 | return (
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
97 |
98 |
99 |
100 |
101 |
105 |
106 |
107 |
108 | );
109 | }
110 |
111 | componentDidMount() {
112 | this._storeListener = this._onStoreChanged.bind(this);
113 | HarStore.listen(this._storeListener);
114 | }
115 |
116 | componentWillUnmount() {
117 | HarStore.unlisten(this._storeListener);
118 | }
119 |
120 | _sampleChanged(har) {
121 | HarActions.loadHar(har);
122 | }
123 |
124 | _onStoreChanged(state) {
125 | this.setState({
126 | activeHar: state.activeHar
127 | });
128 | }
129 |
130 | _onFilterTextChanged(text) {
131 | this.setState({filterText: text});
132 | }
133 |
134 | _onColumnSort(dataKey, direction) {
135 | this.setState({sortKey: dataKey, sortDirection: direction});
136 | }
137 |
138 | _sortEntriesByKey(dataKey, sortDirection, entries) {
139 | if (_.isEmpty(dataKey) || _.isEmpty(sortDirection)) return entries;
140 |
141 | var keyMap = {
142 | url: 'request.url',
143 | time: 'time.start'
144 | };
145 | var getValue = function (entry) {
146 | var key = keyMap[dataKey] || dataKey;
147 | return _.get(entry, key);
148 | };
149 |
150 | var sorted = _.sortBy(entries, getValue); // By default _.sortBy is ascending
151 | if (sortDirection === 'desc') {
152 | sorted.reverse();
153 | }
154 |
155 | return sorted;
156 | }
157 |
158 | _onFilterChanged(type) {
159 | this.setState({filterType: type});
160 | }
161 |
162 | _filterEntries(filter, entries) {
163 | return _.filter(entries, function (x) {
164 | var matchesType = filter.type === 'all' || filter.type === x.type,
165 | matchesText = _.includes(x.request.url, filter.text);
166 |
167 | return matchesType && matchesText;
168 | });
169 | }
170 |
171 | _prepareScale(entries, page) {
172 | var startTime = 0,
173 | lastEntry = _.last(entries),
174 | endTime = lastEntry.time.start + lastEntry.time.total,
175 | maxTime = Math.max(endTime, page.pageTimings.onLoad);
176 |
177 | var scale = d3.scale.linear()
178 | .domain([startTime, Math.ceil(maxTime)])
179 | .range([0, 100]);
180 |
181 | return scale;
182 | }
183 | };
--------------------------------------------------------------------------------
/src/components/SampleSelector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import HarActions from '../store/HarActions';
3 |
4 | const PropTypes = React.PropTypes;
5 |
6 | export default class SampleSelector extends React.Component {
7 |
8 | constructor() {
9 | super();
10 | this.state = {};
11 | }
12 |
13 | render() {
14 | var sampleOptions = _.map(window.samples, (s)=> {
15 | return ({s.label} );
16 | });
17 |
18 | return (
19 |
20 | HAR File
21 |
22 | ---
23 | {sampleOptions}
24 |
25 |
26 | );
27 | }
28 |
29 | _sampleChanged() {
30 | var type = this.refs.selector.getDOMNode().value,
31 | har = type
32 | ? _.find(window.samples, (x)=>x.id === type).har
33 | : null;
34 |
35 | if (this.props.onSampleChanged) {
36 | this.props.onSampleChanged(har);
37 | }
38 | }
39 | };
40 |
41 | SampleSelector.propTypes = {
42 | onSampleChanged: PropTypes.func
43 | };
44 | SampleSelector.defaultProps = {
45 | onSampleChanged: null
46 | };
--------------------------------------------------------------------------------
/src/components/file-type/FileType.jsx:
--------------------------------------------------------------------------------
1 | require('./_file-type.scss');
2 | import React from 'react';
3 |
4 | export default class FileType extends React.Component {
5 |
6 | constructor() {
7 | super();
8 | this.state = {};
9 | }
10 |
11 | render() {
12 | var type = this.props.type;
13 |
14 | return (
15 |
16 | {type}
17 | {this.props.url}
18 |
19 | );
20 | }
21 |
22 | };
23 |
24 | FileType.defaultProps = {
25 | url: null,
26 | type: null
27 | };
28 | FileType.defaultProps = {};
--------------------------------------------------------------------------------
/src/components/file-type/_file-type.scss:
--------------------------------------------------------------------------------
1 | @mixin type($color) {
2 | background: linear-gradient($color, darken($color, 15%));
3 | }
4 |
5 | .fileType {
6 | white-space: nowrap;
7 |
8 | .fileType-url {
9 | display: inline-block;
10 | margin-left: 1rem;
11 | }
12 |
13 | .fileType-type {
14 | border-radius: 3px;
15 | color: white;
16 | padding: 2px 5px;
17 | font-size: 1rem;
18 | display: inline-block;
19 | margin-left: 3px;
20 | font-weight: bold;
21 | @include type(#999);
22 |
23 | &.html {
24 | @include type(#456499);
25 | }
26 | &.css {
27 | @include type(#6b9979);
28 | }
29 | &.js {
30 | @include type(#99993b);
31 | }
32 | &.json {
33 | @include type(#558f99);
34 | }
35 | &.image {
36 | @include type(#866399);
37 | }
38 | &.font {
39 | @include type(#799958);
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/components/har-entry-table/HarEntryTable.jsx:
--------------------------------------------------------------------------------
1 | require('fixed-data-table/dist/fixed-data-table.css');
2 | require('./_har-entry-table.scss');
3 |
4 | import _ from 'lodash';
5 | import React from 'react';
6 | import FixedDataTable from 'fixed-data-table';
7 | import TimeBar from '../timebar/TimeBar.jsx';
8 | import FileType from '../file-type/FileType.jsx';
9 | import formatter from '../../core/formatter';
10 | import {OverlayTrigger, Popover, Tooltip, Button} from 'react-bootstrap';
11 |
12 | const Table = FixedDataTable.Table;
13 | const Column = FixedDataTable.Column;
14 | const GutterWidth = 30;
15 | const PropTypes = React.PropTypes;
16 |
17 | export default class HarEntryTable extends React.Component {
18 |
19 | constructor() {
20 | super();
21 |
22 | this.state = {
23 | highlightRow: -1,
24 | columnWidths: {
25 | url: 500,
26 | size: 100,
27 | time: 200
28 | },
29 | sortDirection: {
30 | url: null,
31 | size: null,
32 | time: null
33 | },
34 | tableWidth: 1000,
35 | tableHeight: 500,
36 | isColumnResizing: false
37 | };
38 | }
39 |
40 | render() {
41 |
42 | return (
43 |
75 | );
76 | }
77 |
78 | _getRowClasses(index) {
79 | var classname = (index === this.state.highlightRow) ? 'active' : '';
80 |
81 | return classname;
82 | }
83 |
84 | _readKey(key, entry) {
85 | var keyMap = {
86 | url: 'request.url',
87 | time: 'time.start'
88 | };
89 |
90 | key = keyMap[key] || key;
91 | return _.get(entry, key);
92 | }
93 |
94 | _getEntry(index) {
95 |
96 | return this.props.entries[index];
97 | }
98 |
99 | _onColumnResized(newColumnWidth, dataKey) {
100 |
101 | var columnWidths = this.state.columnWidths;
102 | columnWidths[dataKey] = newColumnWidth;
103 |
104 | this.setState({columnWidths: columnWidths, isColumnResizing: false});
105 | }
106 |
107 |
108 | _renderSizeColumn(cellData, cellDataKey, rowData, rowIndex, columnData, width) {
109 | return ({formatter.fileSize(cellData)} );
110 | }
111 |
112 | _renderUrlColumn(cellData, cellDataKey, rowData, rowIndex, columnData, width) {
113 | return ();
114 | }
115 |
116 | _renderTimeColumn(cellData, cellDataKey, rowData, rowIndex, columnData, width) {
117 | var start = rowData.time.start,
118 | total = rowData.time.total,
119 | pgTimings = this.props.page.pageTimings;
120 |
121 | return (
122 |
129 | );
130 | }
131 |
132 | //-----------------------------------------
133 | // Table Sorting
134 | //-----------------------------------------
135 | _renderHeader(label, dataKey) {
136 | var dir = this.state.sortDirection[dataKey],
137 | classMap = {
138 | asc: 'glyphicon glyphicon-sort-by-attributes',
139 | desc: 'glyphicon glyphicon-sort-by-attributes-alt'
140 | },
141 | sortClass = dir ? classMap[dir] : '';
142 |
143 | return (
144 |
146 | {label}
147 |
148 |
149 |
150 | );
151 | }
152 |
153 | _columnClicked(dataKey) {
154 | var sortDirections = this.state.sortDirection;
155 | var dir = sortDirections[dataKey];
156 |
157 | if (dir === null) {dir = 'asc'; }
158 | else if (dir === 'asc') {dir = 'desc'; }
159 | else if (dir === 'desc') {dir = null; }
160 |
161 | // Reset all sorts
162 | _.each(_.keys(sortDirections), function (x) {
163 | sortDirections[x] = null;
164 | });
165 |
166 | sortDirections[dataKey] = dir;
167 |
168 | if (this.props.onColumnSort) {
169 | this.props.onColumnSort(dataKey, dir);
170 | }
171 | }
172 |
173 | //-----------------------------------------
174 | // Table Resizing
175 | //-----------------------------------------
176 | componentDidMount() {
177 |
178 | window.addEventListener('resize', this._onResize.bind(this));
179 | this._onResize();
180 | }
181 |
182 | _onResize() {
183 | clearTimeout(this._updateSizeTimer);
184 | this._updateSizeTimer = setTimeout(this._updateSize.bind(this), 50);
185 | }
186 |
187 | _updateSize() {
188 | var parent = React.findDOMNode(this).parentNode;
189 |
190 | this.setState({
191 | tableWidth: parent.clientWidth - GutterWidth,
192 | tableHeight: document.body.clientHeight - parent.offsetTop - GutterWidth*0.5
193 | });
194 | }
195 |
196 | };
197 |
198 | HarEntryTable.defaultProps = {
199 | entries: [],
200 | page: null,
201 | onColumnSort: null,
202 | timeScale: null
203 | };
204 |
205 | HarEntryTable.propTypes = {
206 | entries: PropTypes.array,
207 | page: PropTypes.object,
208 | onColumnSort: PropTypes.func,
209 | timeScale: PropTypes.func
210 | };
211 |
--------------------------------------------------------------------------------
/src/components/har-entry-table/_har-entry-table.scss:
--------------------------------------------------------------------------------
1 | .sortable {
2 | cursor: pointer;
3 | }
4 |
5 | .column-time {
6 |
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/components/pie-chart/ChartBuilder.js:
--------------------------------------------------------------------------------
1 | import mimeTypes from '../../core/mime-types';
2 |
3 | export default function(groups, parentNode, config) {
4 | var radius = Math.min(config.width, config.height) / 2,
5 | arc = d3.svg.arc()
6 | .outerRadius(radius - 10)
7 | .innerRadius(radius / 2),
8 | labelArc = d3.svg.arc()
9 | .outerRadius(radius - 5)
10 | .innerRadius(radius - 5),
11 | pie = d3.layout.pie()
12 | .sort(null)
13 | .value(function (d) { return d.count; });
14 |
15 | var data = pie(groups),
16 | keyFn = (x)=> { return x.data.type; };
17 |
18 | var parent = d3.select(parentNode);
19 |
20 |
21 | // Pie slices -------------------- //
22 | var path = parent.selectAll('path')
23 | .data(data, keyFn);
24 | path
25 | .enter()
26 | .append('path');
27 |
28 | path
29 | .attr('d', arc)
30 | .style('fill', function (d) {
31 | return mimeTypes.types[d.data.type].color;
32 | })
33 | .style('fill-opacity', 0)
34 | .transition()
35 | .duration(500)
36 | .style('fill-opacity', 1);
37 |
38 | path.exit()
39 | .transition()
40 | .duration(500)
41 | .style('fill-opacity', 0)
42 | .remove();
43 |
44 | // Labels ----------------------- //
45 | var text = parent.selectAll('text')
46 | .data(data, keyFn);
47 | text
48 | .enter()
49 | .append("text")
50 | .attr('dy', '0.5em')
51 | .style('font-size', '0.7em')
52 |
53 | text
54 | .transition()
55 | .duration(500)
56 | .attr("transform", (d) => {
57 | var angle = (d.startAngle + d.endAngle) / 2,
58 | degrees = displayAngle(angle);
59 |
60 | if (degrees > 90) {
61 | degrees -= 180;
62 | }
63 |
64 | return `translate(${labelArc.centroid(d)}) rotate(${degrees} 0 0)`;
65 | })
66 | .style("text-anchor", function (d) {
67 | var angle = (d.startAngle + d.endAngle) / 2,
68 | degrees = displayAngle(angle);
69 |
70 | return (degrees > 90 ? 'end' : 'start');
71 | })
72 | .text(function (d) {
73 | var label = mimeTypes.types[d.data.type].label;
74 | return `${label} (${d.data.count})`;
75 | });
76 |
77 | text.exit()
78 | .transition()
79 | .duration(500)
80 | .style('fill-opacity', 0)
81 | .remove();
82 |
83 |
84 | function displayAngle(radians) {
85 | var degrees = (radians * 180) / Math.PI;
86 |
87 | degrees = degrees - 90;
88 |
89 | return degrees;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/pie-chart/TypePieChart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import d3 from 'd3';
3 | import _ from 'lodash';
4 | import chartBuilder from './chartBuilder';
5 |
6 | export default class TypePieChart extends React.Component {
7 |
8 | constructor() {
9 | super();
10 | this.state = {
11 | svgWidth: 275,
12 | svgHeight: 275,
13 | width: 125,
14 | height: 125
15 | };
16 | }
17 |
18 | render() {
19 | var center = {
20 | x: this.state.svgWidth / 2,
21 | y: this.state.svgHeight / 2
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | componentDidMount() {
32 | this._buildChart(this.props.entries)
33 | }
34 |
35 | componentWillReceiveProps(props) {
36 | if (this.props.entries.length !== props.entries.length) {
37 | this._buildChart(props.entries);
38 | }
39 | }
40 |
41 | _buildChart(entries) {
42 | var groups = this._getEntriesByGroup(entries || []),
43 | config = {
44 | width: this.state.width,
45 | height: this.state.height
46 | };
47 |
48 | chartBuilder(groups, this.refs.container.getDOMNode(), config);
49 | }
50 |
51 | _getEntriesByGroup(entries) {
52 | return _.chain(entries)
53 | .groupBy(function (x) {
54 | return x.type;
55 | })
56 | .map(function (g, key) {
57 | return {
58 | type: key,
59 | count: g.length
60 | }
61 | })
62 | .value();
63 | }
64 | };
65 |
66 | TypePieChart.defaultProps = {
67 | entries: null
68 | };
--------------------------------------------------------------------------------
/src/components/timebar/TimeBar.jsx:
--------------------------------------------------------------------------------
1 | require('./_timebar.scss');
2 |
3 | import React from 'react';
4 | import formatter from '../../core/formatter';
5 | import {OverlayTrigger, Popover} from 'react-bootstrap'
6 | import TimingDetails from './TimingDetails.jsx';
7 |
8 | const PropTypes = React.PropTypes;
9 |
10 | export default class TimeBar extends React.Component {
11 |
12 | constructor() {
13 | super();
14 | this.state = {};
15 | }
16 |
17 | render() {
18 | var value = (value)=> {
19 | return `${this.props.scale(value)}%`;
20 | },
21 | bars = [
22 | {
23 | type: 'time',
24 | style: {
25 | left: value(this.props.start),
26 | width: value(this.props.total)
27 | },
28 | className: 'timebar-mark-time'
29 | },
30 | {
31 | type: 'contentLoad',
32 | style: {
33 | left: value(this.props.domContentLoad),
34 | width: 1
35 | },
36 | className: 'timebar-mark-contentLoad'
37 | },
38 | {
39 | type: 'pageLoad',
40 | style: {
41 | left: value(this.props.pageLoad),
42 | width: 1
43 | },
44 | className: 'timebar-mark-pageLoad'
45 | }
46 | ],
47 | label = formatter.time(this.props.total);
48 |
49 |
50 | var barElements = _.chain(bars)
51 | .map(function (b) {
52 | return (
);
53 | })
54 | .value();
55 |
56 | var overlay = (
57 |
58 |
61 |
62 | );
63 |
64 |
65 | return (
66 |
70 |
71 | {barElements}
72 | {label}
73 |
74 |
75 | );
76 | }
77 |
78 | };
79 |
80 | TimeBar.defaultProps = {
81 | scale: null,
82 | start: 0,
83 | total: 0,
84 | timings: null,
85 | domContentLoad: 0,
86 | pageLoad: 0
87 | };
88 |
89 | TimeBar.propTypes = {
90 | scale: PropTypes.func,
91 | start: PropTypes.number,
92 | total: PropTypes.number,
93 | timings: PropTypes.object,
94 | domContentLoad: PropTypes.number,
95 | pageLoad: PropTypes.number
96 | };
--------------------------------------------------------------------------------
/src/components/timebar/TimingDetails.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import formatter from '../../core/formatter';
3 |
4 | export default class TimingDetails extends React.Component {
5 |
6 | constructor() {
7 | super();
8 | this.state = {};
9 | }
10 |
11 | render() {
12 | var {blocked, connect, dns, wait, send, receive} = this.props.timings;
13 |
14 | return (
15 |
16 |
17 | Start
18 | {formatter.time(this.props.start)}
19 |
20 |
21 | Blocked
22 | {formatter.time(blocked)}
23 |
24 |
25 | DNS
26 | {formatter.time(dns)}
27 |
28 |
29 | Connect
30 | {formatter.time(connect)}
31 |
32 |
33 | Sent
34 | {formatter.time(send)}
35 |
36 |
37 | Wait
38 | {formatter.time(wait)}
39 |
40 |
41 | Receive
42 | {formatter.time(receive)}
43 |
44 |
45 | Total
46 | {formatter.time(this.props.total)}
47 |
48 |
49 | );
50 | }
51 |
52 | };
53 |
54 | TimingDetails.defaultProps = {
55 | timings: null,
56 | start: 0,
57 | total: 0
58 | };
--------------------------------------------------------------------------------
/src/components/timebar/_timebar.scss:
--------------------------------------------------------------------------------
1 |
2 | $timebar-color: #5ae666;
3 | $container-height: 30px;
4 | $bar-height: 15px;
5 |
6 | .timebar {
7 | height: $bar-height;
8 | position: relative;
9 |
10 | .timebar-label {
11 | font-size: 1rem;
12 | z-index: 1;
13 | position: relative;
14 | top: -3px;
15 | left: 3px;
16 | }
17 |
18 | .timebar-mark {
19 | position: absolute;
20 | top: 0;
21 | background: linear-gradient(gray, darken(gray, 10%));
22 | height: 100%;
23 | }
24 | .timebar-mark-time {
25 | background: linear-gradient($timebar-color, darken($timebar-color, 20%));
26 | border: 1px solid darken($timebar-color, 20%);
27 | }
28 |
29 | .timebar-mark-contentLoad {
30 | top: -$bar-height/2;
31 | height: $container-height;
32 | background: red;
33 | width: 1px;
34 | }
35 | .timebar-mark-pageLoad {
36 | top: -$bar-height/2;
37 | height: $container-height;
38 | background: blue;
39 | width: 1px;
40 | }
41 | }
42 |
43 | .timing-details tbody > .timing-group > td {
44 | padding: 2px 5px;
45 | border-top: none;
46 | }
--------------------------------------------------------------------------------
/src/core/Entry.js:
--------------------------------------------------------------------------------
1 | import mimeTypes from './mime-types.js';
2 |
3 | export default class Entry {
4 |
5 | constructor(harEntry, page) {
6 | "use strict";
7 |
8 | var startTime = new Date(harEntry.startedDateTime) - new Date(page.startedDateTime);
9 |
10 | // Destructuring to the rescue
11 | var {
12 | time,
13 | request: {url, method},
14 | response: {
15 | content: {size, mimeType}
16 | },
17 | timings
18 | } = harEntry;
19 |
20 | this.request = { url: url, method: method };
21 | this.time = {
22 | start: startTime,
23 | total: time,
24 | details: timings
25 | };
26 | this.size = size;
27 | this.type = mimeTypes.identify(mimeType);
28 |
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/core/Page.js:
--------------------------------------------------------------------------------
1 | export default class Page {
2 | constructor(harPage) {
3 | "use strict";
4 |
5 | this.id = harPage.id;
6 | this.startedDateTime = harPage.startedDateTime;
7 | this.pageTimings = _.clone(harPage.pageTimings);
8 | this.entries = [];
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/core/formatter.js:
--------------------------------------------------------------------------------
1 | export default {
2 |
3 | // Adapted from: http://stackoverflow.com/a/14919494/419712
4 | fileSize(bytes) {
5 | var thresh = 1024;
6 | if (Math.abs(bytes) < thresh) {
7 | return `${bytes} B`;
8 | }
9 | var units = ['kB', 'MB', 'GB', 'TB'];
10 | var u = -1;
11 | do {
12 | bytes /= thresh;
13 | ++u;
14 | } while (Math.abs(bytes) >= thresh && u < units.length - 1);
15 |
16 | return `${bytes.toFixed(1)} ${units[u]}`;
17 | },
18 |
19 | time(ms) {
20 | var time = Math.round(ms);
21 |
22 | if (time < 0) {
23 | return '--';
24 | }
25 |
26 | if (time < 1000) {
27 | return `${time} ms`;
28 | }
29 |
30 | return `${time / 1000} s`;
31 | }
32 | };
--------------------------------------------------------------------------------
/src/core/har-parser.js:
--------------------------------------------------------------------------------
1 | var _ = require('lodash');
2 |
3 | import Page from './Page.js'
4 | import Entry from './Entry.js'
5 |
6 | export default {
7 |
8 | parse: parse
9 | };
10 |
11 | function parse(har) {
12 | "use strict";
13 |
14 | var pageMap = {},
15 | pages = [];
16 |
17 | _.each(har.log.pages, function (p) {
18 | var page = new Page(p);
19 | pageMap[p.id] = page;
20 | pages.push(page);
21 | });
22 |
23 | _.each(har.log.entries, function (p) {
24 | var page = pageMap[p.pageref],
25 | entry = new Entry(p, page);
26 |
27 | page.entries.push(entry);
28 | });
29 |
30 | return pages;
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/src/core/mime-types.js:
--------------------------------------------------------------------------------
1 | var types = {
2 | json: {
3 | label: 'XHR',
4 | color: '#558f99',
5 | mime: [
6 | 'application/json'
7 | ]
8 | },
9 | js: {
10 | label: 'Script',
11 | color: '#99993b',
12 | mime: [
13 | 'application/javascript',
14 | 'text/javascript'
15 | ]
16 | },
17 | css: {
18 | label: 'Style',
19 | color: '#6b9979',
20 | mime: [
21 | 'text/css'
22 | ]
23 | },
24 | image: {
25 | label: 'Image',
26 | color: '#866399',
27 | mime: [
28 | 'image/jpg',
29 | 'image/jpeg',
30 | 'image/png',
31 | 'image/gif',
32 | 'image/bmp',
33 | 'image/svg+xml'
34 | ]
35 | },
36 | font: {
37 | label: 'Font',
38 | color: '#799958',
39 | mime: [
40 | 'application/font-woff',
41 | 'application/font-ttf',
42 | 'application/vnd.ms-fontobject',
43 | 'application/font-otf'
44 | ]
45 | },
46 | html: {
47 | label: 'Document',
48 | color: '#456499',
49 | mime: [
50 | 'text/html'
51 | ]
52 | },
53 | other: {
54 | label: 'Other',
55 | color: '#999999',
56 | mime: []
57 | }
58 | };
59 |
60 |
61 | export default {
62 | types: types,
63 | identify: identify
64 | };
65 |
66 | function identify(mimeType) {
67 | "use strict";
68 |
69 | var fileType = _.find(_.keys(types), function (type) {
70 | return _.includes(types[type].mime, mimeType);
71 | });
72 |
73 | return fileType || 'other';
74 | }
75 |
--------------------------------------------------------------------------------
/src/samples.js:
--------------------------------------------------------------------------------
1 | window.samples = [
2 | {
3 | id: 'so',
4 | har: require('./store/sample-hars/stackoverflow.com.json'),
5 | label: 'Stack Overflow'
6 | },
7 | {
8 | id: 'nyt',
9 | har: require('./store/sample-hars/www.nytimes.com.json'),
10 | label: 'New York Times'
11 | },
12 | {
13 | id: 'react',
14 | har: require('./store/sample-hars/facebook.github.io.json'),
15 | label: 'Facebook React'
16 | }
17 | ];
18 |
--------------------------------------------------------------------------------
/src/store/HarActions.js:
--------------------------------------------------------------------------------
1 | import alt from './alt';
2 |
3 | class HarActions {
4 |
5 | loadUrl(url) {
6 | fetch(url)
7 | .then((response) => {
8 | return response.json();
9 | })
10 | .then((data) => {
11 | this.dispatch(data)
12 | });
13 | }
14 |
15 | loadHar(har) {
16 | this.dispatch(har);
17 | }
18 | }
19 |
20 | export default alt.createActions(HarActions);
--------------------------------------------------------------------------------
/src/store/HarStore.js:
--------------------------------------------------------------------------------
1 | import alt from './alt';
2 | import HarActions from './HarActions';
3 |
4 |
5 | class HarStore {
6 |
7 | constructor() {
8 | this.bindListeners({
9 | loadUrl: HarActions.loadUrl,
10 | loadHar: HarActions.loadHar
11 | });
12 |
13 | this.state = {
14 | hars: [],
15 | activeHar: null
16 | };
17 | }
18 |
19 | loadUrl(json) {
20 | this._setState(json);
21 | }
22 |
23 | loadHar(json) {
24 | this._setState(json);
25 | }
26 |
27 | _setState(json) {
28 | this.setState({
29 | hars: this.state.hars.concat(json),
30 | activeHar: json
31 | });
32 | }
33 | }
34 |
35 | export default alt.createStore(HarStore, 'HarStore');
--------------------------------------------------------------------------------
/src/store/alt.js:
--------------------------------------------------------------------------------
1 | import Alt from 'alt';
2 | export default new Alt();
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: {
6 | app: ['./src/app.jsx'],
7 | samples: ['./src/samples.js']
8 | },
9 | output: {
10 | path: './build',
11 | filename: '[name].bundle.js'
12 | },
13 |
14 | module: {
15 | loaders: [
16 | {
17 | test: /\.css$/,
18 | loader: 'style!' + 'css?sourceMap'
19 | },
20 | {
21 | test: /\.scss$/,
22 | loader: 'style!' + 'css?sourceMap' + '!sass?sourceMap'
23 | },
24 | {
25 | test: /\.(js|jsx)$/,
26 | exclude: /node_modules/,
27 | loader: 'babel-loader?optional[]=runtime'
28 | },
29 | {
30 | test: /\.(json)$/,
31 | exclude: /node_modules/,
32 | loader: 'json-loader'
33 | },
34 |
35 | {
36 | test: /\.(svg|ttf|woff|woff2|eot)(\?v=\d+\.\d+\.\d+)?$/,
37 | loader: "url-loader"
38 | }
39 |
40 | ]
41 | },
42 |
43 | plugins: [
44 | new HtmlWebpackPlugin({
45 | title: 'HAR Viewer (React Deep-Dive)'
46 | })
47 | ],
48 |
49 | devtool: 'source-map'
50 |
51 | };
--------------------------------------------------------------------------------