├── .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 | 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 (); 16 | }); 17 | 18 | return ( 19 |
20 | 21 | 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 | 52 | 60 | 67 | 74 |
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 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
Start{formatter.time(this.props.start)}
Blocked{formatter.time(blocked)}
DNS{formatter.time(dns)}
Connect{formatter.time(connect)}
Sent{formatter.time(send)}
Wait{formatter.time(wait)}
Receive{formatter.time(receive)}
Total{formatter.time(this.props.total)}
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 | }; --------------------------------------------------------------------------------