├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── bin └── copy-core.js ├── deploy └── deploy.js ├── docs ├── TODO.md └── wip-tests.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── webapp ├── css │ ├── SyntaxHighlighter.css │ ├── aboutTab.css │ ├── domTab.css │ ├── domTree.css │ ├── dragdrop.css │ ├── harPreview.css │ ├── harStats.css │ ├── harView.css │ ├── harViewer.css │ ├── highlightjs │ │ ├── default.min.css │ │ └── tomorrow.min.css │ ├── homeTab.css │ ├── images │ │ ├── ajax-loader.gif │ │ ├── bg-button.gif │ │ ├── blank.gif │ │ ├── button-background.png │ │ ├── checkmark.gif │ │ ├── checkmark.png │ │ ├── close-sprites.png │ │ ├── contextMenuTarget.png │ │ ├── contextMenuTargetHover.png │ │ ├── download-sprites.png │ │ ├── downloadButtons-aero.png │ │ ├── group.gif │ │ ├── loading_16.gif │ │ ├── menu │ │ │ ├── previewMenuHandle.png │ │ │ ├── shadowAlpha.png │ │ │ ├── tabMenuCheckbox.png │ │ │ ├── tabMenuPin.png │ │ │ └── tabMenuRadio.png │ │ ├── netBarBlocking.gif │ │ ├── netBarBlocking2.gif │ │ ├── netBarCached.gif │ │ ├── netBarConnecting.gif │ │ ├── netBarLoaded.gif │ │ ├── netBarReceiving.gif │ │ ├── netBarResolving.gif │ │ ├── netBarResponded.gif │ │ ├── netBarSending.gif │ │ ├── netBarWaiting.gif │ │ ├── page-timeline.png │ │ ├── save.png │ │ ├── splitterh.png │ │ ├── spriteArrows.gif │ │ ├── spriteArrows.png │ │ ├── tabEnabled.png │ │ ├── timeline-sprites.png │ │ ├── tooltipConnectorUp.png │ │ └── twisty-sprites.png │ ├── infoTip.css │ ├── pageList.css │ ├── pageTimeline.css │ ├── popupMenu.css │ ├── previewMenu.css │ ├── previewTab.css │ ├── requestBody.css │ ├── requestList.css │ ├── schemaTab.css │ ├── search.css │ ├── tabView.css │ ├── tableView.css │ ├── toolTip.css │ ├── toolbar.css │ ├── validationError.css │ └── xhrSpy.css ├── demo.js ├── demos │ └── demo-shell.html ├── examples │ ├── browser-blocking-time.har │ ├── embed.html │ ├── google.com.har │ ├── inline-scripts-block.har │ ├── inline-scripts-block.harp │ ├── inline-scripts-block.harp.js │ └── softwareishard.com.har ├── har.js ├── highlightjs │ └── highlight.min.js ├── index.html ├── loader.html ├── main.js ├── modules │ ├── App.js │ ├── AppContext.js │ ├── Demo.js │ ├── InfoTip.js │ ├── InfoTipHolder.js │ ├── Stats.js │ ├── booleanFlipper.js │ ├── buildInfo.js │ ├── core │ │ ├── StatsService.js │ │ ├── array.js │ │ ├── cookies.js │ │ ├── css.js │ │ ├── date.js │ │ ├── dom.js │ │ ├── dragdrop.js │ │ ├── events.js │ │ ├── json.js │ │ ├── lib.js │ │ ├── mime.js │ │ ├── object.js │ │ ├── rect.js │ │ ├── sniff.js │ │ ├── string.js │ │ ├── trace.js │ │ └── url.js │ ├── deferred.js │ ├── intersperse.js │ ├── json-query │ │ └── JSONQuery.js │ ├── nettable │ │ ├── Bar.js │ │ ├── NetInfoRow.js │ │ ├── NetRow.js │ │ ├── NetSummaryRow.js │ │ ├── NetSummaryRowModel.js │ │ ├── NetTable.js │ │ ├── PageTimingBar.js │ │ ├── Phases.js │ │ ├── defaultTimingDefinitions.js │ │ └── testData.js │ ├── nls │ │ ├── domTab.js │ │ ├── harModel.js │ │ ├── harStats.js │ │ ├── harViewer.js │ │ ├── homeTab.js │ │ ├── pageList.js │ │ ├── pageTimeline.js │ │ ├── previewTab.js │ │ ├── requestBody.js │ │ ├── requestList.js │ │ ├── search.js │ │ └── tableView.js │ ├── pagetable │ │ ├── PageInfoRow.js │ │ ├── PageRow.js │ │ └── PageTable.js │ ├── pagetimeline │ │ ├── PageDescContainer.js │ │ ├── PageTimeline.js │ │ ├── PageTimelineCol.js │ │ ├── PageTimelineTable.js │ │ └── Selection.js │ ├── pie │ │ ├── CachePie.js │ │ ├── ContentPie.js │ │ ├── Pie.js │ │ ├── TimingPie.js │ │ └── TrafficPie.js │ ├── preview │ │ ├── harModel.js │ │ ├── harModelLoader.js │ │ ├── harSchema.js │ │ ├── jsonSchema.js │ │ └── ref.js │ ├── requestbodies │ │ ├── DataURL.js │ │ ├── ExternalImage.js │ │ ├── Headers.js │ │ ├── Highlighted.js │ │ ├── Image.js │ │ ├── JSONEntryTree.js │ │ ├── ParamRow.js │ │ ├── PlainResponse.js │ │ ├── SendData.js │ │ ├── TitleRow.js │ │ ├── URLParameters.js │ │ ├── XMLEntryTree.js │ │ └── decoder.js │ ├── setState.js │ ├── tabs │ │ ├── AboutTab.js │ │ ├── HomeTab.js │ │ ├── ObjectSearch.js │ │ ├── PreviewTab.js │ │ ├── PreviewTabToolbar.js │ │ ├── SchemaTab.js │ │ ├── aboutTab.html │ │ ├── dom │ │ │ ├── DOMBox.js │ │ │ ├── DOMTab.js │ │ │ └── SearchBox.js │ │ ├── homeTab.html │ │ └── preview │ │ │ ├── PreviewList.js │ │ │ └── ValidationError.js │ ├── tabview │ │ ├── Tab.js │ │ ├── TabBar.js │ │ ├── TabBodies.js │ │ ├── TabBody.js │ │ └── TabView.js │ ├── timeinfotip │ │ └── TimeInfoTip.js │ ├── toolbar │ │ └── Toolbar.js │ └── tree │ │ ├── ObjectTree.js │ │ ├── Tree.js │ │ ├── TreeRepresentations.js │ │ └── XMLTree.js └── preview.html └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | # Don't lint dist 2 | webapp/dist/**/* 3 | 4 | # Don't lint the old code yet 5 | webapp/har.js 6 | webapp/modules/**/nls/* 7 | webapp/modules/core/**/* 8 | webapp/modules/preview/harModel.js 9 | webapp/modules/preview/harModelLoader.js 10 | webapp/modules/preview/harSchema.js 11 | webapp/modules/preview/jsonSchema.js 12 | webapp/modules/preview/ref.js 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "google", 5 | "plugin:react/recommended", 6 | ], 7 | "settings": { 8 | "react": { 9 | "version": "16.6.0", 10 | }, 11 | }, 12 | "plugins": [ 13 | "babel", 14 | "react", 15 | ], 16 | "rules": { 17 | "indent": ["error", 2], 18 | "linebreak-style": "off", 19 | 20 | "max-len": ["warn", { 21 | "code": 100, 22 | "tabWidth": 2, 23 | "ignoreUrls": true, 24 | // JSX expression: Line starting with spaces then "<" 25 | // JSX assignment: Line containing " = <\w" 26 | // JSX return: Line starting with "return <\w" 27 | "ignorePattern": "^ *<| = <\\w|^ *return <\\w", 28 | }], 29 | 30 | // E.g. to allow 31 | //
this.holder = ref}> 32 | "no-return-assign": "warn", 33 | 34 | "object-curly-spacing": ["error", "always"], 35 | 36 | "quotes": ["error", "double", { "allowTemplateLiterals": true }], 37 | 38 | "require-jsdoc": "off", 39 | 40 | // Use "babel/no-invalid-this" because we use ES7 class properties 41 | "no-invalid-this": "off", 42 | "babel/no-invalid-this": "error", 43 | }, 44 | "env": { 45 | "browser": true, 46 | "jquery": true, 47 | }, 48 | "parserOptions": { 49 | "ecmaVersion": 6, 50 | "sourceType": "module", 51 | "ecmaFeatures": { 52 | "jsx": true, 53 | "classes": true, 54 | "defaultParams": true, 55 | "experimentalObjectRestSpread": true 56 | }, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | typings/ 3 | temp/ 4 | dist/ 5 | *.log 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.detectIndentation": false, 4 | "editor.tabSize": 2, 5 | "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 gitgrimbo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # harviewer-react 2 | 3 | 4 | 5 | WARNING - harviewer-react is under development. Until a 1.0.0 release, branches can and will be rebased (I will experiment with things and remove things). 6 | 7 | As this repo currently only has a single contributor (me) this isn't a problem. 8 | 9 | If you would like to start contributing, then I will choose a less destructive approach. 10 | 11 | 12 | 13 | --- 14 | 15 | harviewer-react is [HAR Viewer](https://github.com/janodvarko/harviewer) 16 | implemented in [React](https://facebook.github.io/react/)! 17 | 18 | Use it at [https://gitgrimbo.github.io/harviewer-react/](https://gitgrimbo.github.io/harviewer-react/). 19 | 20 | # Develop 21 | 22 | I use [Visual Studio Code](https://code.visualstudio.com). 23 | 24 | # Running the code 25 | 26 | harviewer-react uses [webpack](https://webpack.github.io/). 27 | 28 | ## Setup 29 | 30 | Using [npm](https://www.npmjs.com/), pull in the required packages with: 31 | 32 | npm install 33 | 34 | ## Start 35 | 36 | To start the [webpack-dev-server](https://webpack.github.io/docs/webpack-dev-server.html), use: 37 | 38 | npm start 39 | 40 | The Demo Shell will now be available at: 41 | 42 | - http://localhost:8080/webapp/demos/demo-shell.html 43 | 44 | And the main app will be available at: 45 | 46 | - http://localhost:8080/webapp/ 47 | 48 | # Deploy 49 | 50 | npm run deploy-gh-pages 51 | 52 | This will build the production version of harviewer-react and deploy it as a 53 | [GitHub pages](https://pages.github.com/) site. 54 | -------------------------------------------------------------------------------- /bin/copy-core.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copies code from old HAR Viewer to new. 3 | */ 4 | const fs = require("fs-extra"); 5 | const path = require("path"); 6 | const mkdirp = require("mkdirp"); 7 | const glob = require("glob"); 8 | 9 | const args = process.argv.slice(2); 10 | const hvDir = args[0] || "../harviewer/"; 11 | 12 | const copyConfig = { 13 | scripts: { 14 | from: path.resolve(hvDir, "webapp", "scripts"), 15 | to: path.resolve(__dirname, "../webapp", "modules"), 16 | patterns: [ 17 | "core/**", 18 | "nls/**", 19 | "tabs/*.html", 20 | "tabs/ObjectSearch.js", 21 | "preview/**", 22 | "json-query/JSONQuery.js", 23 | ], 24 | }, 25 | css: { 26 | from: path.resolve(hvDir, "webapp", "css"), 27 | to: path.resolve(__dirname, "../webapp", "css"), 28 | patterns: [ 29 | "*.css", 30 | ], 31 | }, 32 | }; 33 | 34 | Object.keys(copyConfig).forEach((configKey) => { 35 | console.log(`Copying files for "${configKey}"`); 36 | const config = copyConfig[configKey]; 37 | 38 | if (!fs.pathExistsSync(config.from)) { 39 | throw new Error(`from path "${config.from}" does not exist`); 40 | } 41 | if (!fs.pathExistsSync(config.to)) { 42 | throw new Error(`to path "${config.to}" does not exist`); 43 | } 44 | 45 | config.patterns.forEach((pattern) => { 46 | const files = glob.sync(pattern, { 47 | cwd: config.from, 48 | }); 49 | files.forEach((file) => { 50 | const src = path.resolve(config.from, file); 51 | const dest = path.resolve(config.to, file); 52 | if (fs.statSync(src).isFile()) { 53 | mkdirp.sync(path.dirname(dest)); 54 | fs.copySync(src, dest); 55 | console.log(`Copied ${src} to ${dest}`); 56 | } 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /deploy/deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, shelljs */ 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | require("shelljs/global"); 5 | 6 | const ghTemp = "./temp/gh-pages/"; 7 | const repo = require("../package.json").repository.url; 8 | 9 | set("+v"); 10 | 11 | if (!which("git")) { 12 | echo("git is required"); 13 | exit(1); 14 | } 15 | 16 | 17 | // Clean and create the staging folder, ghTemp. 18 | 19 | rm("-rf", ghTemp); 20 | mkdir("-p", ghTemp); 21 | 22 | if (exec("git clone " + repo + " --branch gh-pages --single-branch " + ghTemp).code !== 0) { 23 | echo("git clone failed"); 24 | exit(1); 25 | } 26 | 27 | 28 | // Run the build and copy the built resources to staging 29 | 30 | if (exec("npm run build:prod").code !== 0) { 31 | echo("webpack build failed"); 32 | exit(1); 33 | } 34 | 35 | cp("-r", "./dist/*", ghTemp); 36 | 37 | 38 | // Copy the webapp to staging 39 | 40 | cp("-r", "./webapp/*", ghTemp); 41 | 42 | 43 | // Copy other required node_modules to staging 44 | 45 | function copyAndPreserveFolder(dest, folder, file) { 46 | const src = path.join(folder, file); 47 | mkdir("-p", path.join(dest, folder)); 48 | cp(src, path.join(dest, folder)); 49 | } 50 | 51 | copyAndPreserveFolder(ghTemp, "node_modules/jquery/dist", "jquery.js"); 52 | copyAndPreserveFolder(ghTemp, "node_modules/urijs/src", "URI.js"); 53 | 54 | 55 | // Fix the path differences between dev and prod. 56 | 57 | function fixLinks(file) { 58 | // When deploying we don't want to move 'out' of the webapp to locate the 59 | // node_modules folder. 60 | let str = fs.readFileSync(file, { encoding: "utf-8" }); 61 | str = str.replace("../node_modules/", "node_modules/"); 62 | fs.writeFileSync(file, str); 63 | } 64 | 65 | fixLinks(path.join(ghTemp, "index.html")); 66 | fixLinks(path.join(ghTemp, "demos", "demo-shell.html")); 67 | 68 | // https://github.com/blog/572-bypassing-jekyll-on-github-pages 69 | touch(path.join(ghTemp, ".nojekyll")); 70 | 71 | // Add the staging files to the gh-pages branch 72 | 73 | cd(ghTemp); 74 | 75 | exec("git add ."); 76 | exec("git commit --amend --no-edit"); 77 | exec("git push -f origin gh-pages"); 78 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - remove jQuery? 4 | -------------------------------------------------------------------------------- /docs/wip-tests.md: -------------------------------------------------------------------------------- 1 | # General questions 2 | 3 | - Is there a simple way for test fixture HTML pages to support both old and new? 4 | - Unlikely as they import different resources. e.g.: 5 | - old - jquery/requirejs/old harviewer 6 | - new - jquery/vendor.js/main.js/prismjs 7 | 8 | ## testRemoveTab 9 | 10 | Reasons for failure 11 | - test page `testRemoveTabIndex.html` is legacy, doesn't import hv-react in the new way. 12 | 13 | ## testHideTabBar 14 | 15 | Reasons for failure 16 | - test page `testHideTabBarIndex.html` is legacy, doesn't import hv-react in the new way. 17 | 18 | ## testShowStatsAndTimeline 19 | 20 | Reasons for failure 21 | - test page `testShowStatsAndTimelineIndex.html` is legacy, doesn't import hv-react in the new way. 22 | 23 | ## testCustomPageTiming 24 | 25 | Reasons for failure 26 | - test page `testCustomPageTimingIndex.html` is legacy, doesn't import hv-react in the new way. 27 | 28 | ## testPhases 29 | 30 | Reasons for failure 31 | - phases aren't supported yet in hv-react. 32 | 33 | ## testLoadHarAPI 34 | 35 | Reasons for failure 36 | - test pages `testLoadHarAPIViewer.html`, `testLoadArchives.html`, `testLoadHarAPIPreview.html` is legacy, doesn't import hv-react in the new way. 37 | 38 | ## testCustomizeColumns 39 | 40 | Reasons for failure 41 | - test pages `testCustomizeColumnsPage.html`, `testCustomizeColumnsPage2.html`, `testCustomizeColumnsPage3.html` is legacy, doesn't import hv-react in the new way. 42 | 43 | # Failing tests 44 | 45 | ``` 46 | Total: [✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓××××××✓] 144/144 47 | Passed: 128 Failed: 16 Skipped: 0 48 | 49 | Chr 71 Win: [✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓××××××✓] 144/144, 16 fail 50 | 51 | × chrome 71.0.3578.80 on Windows NT - testRemoveTab - testRemoveTab 52 | × chrome 71.0.3578.80 on Windows NT - testHideTabBar - testHideTabBar 53 | × chrome 71.0.3578.80 on Windows NT - testShowStatsAndTimeline - testShowStatsAndTimeline 54 | × chrome 71.0.3578.80 on Windows NT - testCustomPageTiming - testCustomPageTiming 55 | × chrome 71.0.3578.80 on Windows NT - testPhases - testPhases 56 | × chrome 71.0.3578.80 on Windows NT - testLoadHarAPI - testViewer 57 | × chrome 71.0.3578.80 on Windows NT - testLoadHarAPI - testViewer loadArchives 58 | × chrome 71.0.3578.80 on Windows NT - testLoadHarAPI - testPreview 59 | × chrome 71.0.3578.80 on Windows NT - testCustomizeColumns - 1 60 | × chrome 71.0.3578.80 on Windows NT - testCustomizeColumns - 2 61 | × chrome 71.0.3578.80 on Windows NT - testCustomizeColumns - 3 62 | × chrome 71.0.3578.80 on Windows NT - testSearchHAR - testSearchHAR 63 | × chrome 71.0.3578.80 on Windows NT - testPreviewExpand - testExpandSinglePage 64 | × chrome 71.0.3578.80 on Windows NT - testPreviewExpand - testExpandMultiplePages 65 | × chrome 71.0.3578.80 on Windows NT - testPreviewExpand - testExpandByDefault 66 | × chrome 71.0.3578.80 on Windows NT - testSearchJsonQuery - testSearchJsonQuery 67 | ``` 68 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "harviewer-react", 3 | "version": "1.0.0", 4 | "description": "HAR Viewer built using React", 5 | "main": "index.js", 6 | "scripts": { 7 | "copy:core": "node bin/copy-core.js ../../harviewer2/", 8 | "deploy-gh-pages": "node deploy/deploy.js", 9 | "lint": "eslint ./webapp", 10 | "start": "webpack-dev-server --disableHostCheck=true --inline --hot --compress --content-base ./", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "build": "webpack --mode=development", 13 | "build:prod": "webpack --mode=production" 14 | }, 15 | "author": { 16 | "name": "gitgrimbo", 17 | "email": "gitgrimbo@gmail.com", 18 | "url": "https://github.com/gitgrimbo" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/gitgrimbo/harviewer-react.git" 23 | }, 24 | "bugs": "https://github.com/gitgrimbo/harviewer-react/issues", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@babel/core": "^7.2.2", 28 | "@babel/preset-env": "^7.3.1", 29 | "@babel/preset-react": "^7.0.0", 30 | "amdi18n-loader": "^0.9.1", 31 | "babel-core": "^6.26.3", 32 | "babel-eslint": "^10.0.1", 33 | "babel-loader": "^8.0.5", 34 | "babel-plugin-transform-class-properties": "^6.24.1", 35 | "babel-plugin-transform-object-assign": "^6.22.0", 36 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 37 | "babel-plugin-transform-runtime": "^6.23.0", 38 | "babel-preset-env": "^1.7.0", 39 | "babel-preset-es2015": "^6.24.1", 40 | "babel-preset-react": "^6.24.1", 41 | "bundle-loader": "^0.5.6", 42 | "core-js": "^2.6.3", 43 | "css-loader": "^2.1.0", 44 | "eslint": "^5.12.1", 45 | "eslint-config-google": "^0.11.0", 46 | "eslint-plugin-babel": "^5.3.0", 47 | "eslint-plugin-react": "^7.12.4", 48 | "file-loader": "^3.0.1", 49 | "fs-extra": "^7.0.1", 50 | "git-revision-webpack-plugin": "^3.0.3", 51 | "glob": "^7.1.3", 52 | "js-cookie": "^2.2.0", 53 | "mkdirp": "^0.5.1", 54 | "raw-loader": "^1.0.0", 55 | "shelljs": "^0.8.3", 56 | "style-loader": "^0.23.1", 57 | "urijs": "^1.19.1", 58 | "url-loader": "^1.1.2", 59 | "webpack": "^4.29.0", 60 | "webpack-bundle-analyzer": "^3.0.3", 61 | "webpack-cli": "^3.2.1", 62 | "webpack-dev-server": "^3.1.14" 63 | }, 64 | "dependencies": { 65 | "@babel/polyfill": "^7.2.5", 66 | "babel-polyfill": "^6.26.0", 67 | "classnames": "^2.2.6", 68 | "file-saver": "^2.0.0", 69 | "jquery": "^3.3.1", 70 | "prop-types": "^15.6.2", 71 | "react": "^16.7.0", 72 | "react-dom": "^16.7.0", 73 | "react-router": "^4.3.1", 74 | "valuelink": "^1.5.5", 75 | "whatwg-fetch": "^3.0.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /webapp/css/aboutTab.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .AboutTab { 4 | margin-left: 100px; 5 | } 6 | 7 | .AboutTab .version { 8 | font-size: 11px; 9 | color: #DD467B; 10 | } 11 | 12 | .tabAboutBody { 13 | font-family: Verdana,Geneva,Arial,Helvetica,sans-serif; 14 | font-size: 11.7px; 15 | font-style: normal; 16 | font-weight: 400; 17 | } 18 | 19 | .aboutBody { 20 | padding: 8px; 21 | } 22 | 23 | .tabAboutBody code { 24 | color: green; 25 | } 26 | -------------------------------------------------------------------------------- /webapp/css/domTab.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .tabDOMBody { 4 | font-family: Lucida Grande,Tahoma,sans-serif; 5 | font-size: 11px; 6 | } 7 | 8 | .tabDOMBody .domContent { 9 | position: absolute; 10 | top: 28px; /* tab view bar + toolbar */ 11 | bottom: 0px; 12 | overflow: auto; 13 | width: 100%; 14 | } 15 | 16 | .tabDOMBody .domContent .domBox { 17 | width: 100%; 18 | } 19 | 20 | .tabDOMBody .domContent .domBox .title { 21 | color: gray; 22 | padding: 8px 0 0 8px; 23 | } 24 | 25 | .tabDOMBody .separator { 26 | border-bottom: 1px solid #D7D7D7; 27 | } 28 | 29 | .tabDOMBody .domTable { 30 | padding: 5px; 31 | } 32 | 33 | /*************************************************************************************************/ 34 | /* Toolbar */ 35 | 36 | .domToolbar > .toolbar { 37 | text-align: right; 38 | } 39 | 40 | /*************************************************************************************************/ 41 | /* Search Results */ 42 | 43 | .tabDOMBody .domContent .domBox .content, 44 | .tabDOMBody .domContent .domBox .results { 45 | vertical-align: top; 46 | } 47 | 48 | .resultsDefaultContent { 49 | color: #D7D7D7; 50 | font-size: 12px; 51 | font-family: Lucida Grande,Tahoma,sans-serif; 52 | margin: 60px; 53 | text-align: center; 54 | } 55 | 56 | .queryResultsViewType { 57 | border-bottom: 1px solid #EEEEEE; 58 | display: block; 59 | padding: 5px; 60 | } 61 | 62 | .queryResultsViewType .type { 63 | width: 13px; 64 | height: 13px; 65 | padding: 0; 66 | margin: 0; 67 | vertical-align: bottom; 68 | } 69 | 70 | .queryResultsViewType .label { 71 | padding-left: 5px; 72 | } 73 | 74 | /*************************************************************************************************/ 75 | /* Splitter */ 76 | 77 | .domBox .splitter { 78 | width: 4px; 79 | cursor: e-resize; 80 | background-color: #D7D7D7; 81 | } 82 | 83 | .domBox .splitter, 84 | .domBox .results { 85 | visibility: collapse; 86 | } 87 | 88 | .domBox .splitter.visible, 89 | .domBox .results.visible { 90 | visibility: visible; 91 | } 92 | -------------------------------------------------------------------------------- /webapp/css/domTree.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .domTable { 4 | } 5 | 6 | .memberLabelCell { 7 | padding: 2px 50px 2px 0; 8 | vertical-align: top; 9 | } 10 | 11 | .memberValueCell { 12 | padding: 1px 0 1px 5px; 13 | display: block; 14 | overflow: hidden; 15 | } 16 | 17 | .memberLabel { 18 | cursor: default; 19 | -moz-user-select: none; 20 | overflow: hidden; 21 | /*position: absolute;*/ 22 | padding-left: 18px; 23 | /*max-width: 30%;*/ 24 | white-space: nowrap; 25 | /* background-color: #FFFFFF; breaks row highlighting */ 26 | } 27 | 28 | .memberRow.hasChildren.opened > .memberLabelCell > .memberLabel { 29 | background-image: url(images/twisty-sprites.png); 30 | background-position: 3px -16px; 31 | background-color: transparent; 32 | } 33 | 34 | .memberRow.hasChildren > .memberLabelCell > .memberLabel:hover { 35 | cursor: pointer; 36 | color: blue; 37 | text-decoration: underline; 38 | } 39 | 40 | .memberRow.hasChildren > .memberLabelCell > .memberLabel { 41 | background-image: url(images/twisty-sprites.png); 42 | background-repeat: no-repeat; 43 | background-position: 3px 3px; 44 | } 45 | 46 | /*************************************************************************************************/ 47 | 48 | .jumpHighlight { 49 | background-color: #C4F4FF !important; 50 | } 51 | 52 | /*************************************************************************************************/ 53 | 54 | .objectBox-object { 55 | color: gray; 56 | } 57 | 58 | .objectBox-number { 59 | color: #000088; 60 | } 61 | 62 | .objectBox-string { 63 | color: #FF0000; 64 | white-space: pre-wrap; 65 | } 66 | 67 | .objectBox-null, 68 | .objectBox-undefined { 69 | font-style: italic; 70 | color: #787878; 71 | } 72 | 73 | .objectBox-array { 74 | color: gray; 75 | } 76 | 77 | /*************************************************************************************************/ 78 | -------------------------------------------------------------------------------- /webapp/css/dragdrop.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | body[vResizing="true"] * { 4 | cursor: e-resize !important; 5 | } 6 | 7 | body[hResizing="true"] * { 8 | cursor: s-resize !important; 9 | } 10 | -------------------------------------------------------------------------------- /webapp/css/harPreview.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | @import url("harView.css"); 4 | 5 | /* Domplate Widgets */ 6 | @import url("tabView.css"); 7 | @import url("toolbar.css"); 8 | @import url("domTree.css"); 9 | @import url("infoTip.css"); 10 | @import url("popupMenu.css"); 11 | /* @import url("tableView.css"); -- preview does not support tableView (part of JSON Query UI) */ 12 | 13 | /* HAR Preview */ 14 | @import url("pageList.css"); 15 | @import url("requestList.css"); 16 | @import url("requestBody.css"); 17 | 18 | @import url("previewMenu.css"); 19 | 20 | @import url("harStats.css"); 21 | @import url("pageTimeline.css"); 22 | 23 | @import url("validationError.css"); 24 | 25 | /* Application Tabs */ 26 | /* @import url("aboutTab.css"); -- viewer mode only */ 27 | /* @import url("homeTab.css"); -- viewer mode only */ 28 | /* @import url("domTab.css"); -- viewer mode only */ 29 | /* @import url("schemaTab.css"); -- viewer mode only */ 30 | /* @import url("previewTab.css"); -- viewer mode only */ 31 | /* @import url("search.css"); -- viewer mode only */ 32 | 33 | /* highlight.js */ 34 | @import url("highlightjs/tomorrow.min.css"); 35 | 36 | /* @import url("dragdrop.css"); -- viewer mode only */ 37 | -------------------------------------------------------------------------------- /webapp/css/harStats.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .pageStatsBody[class~=opened] { 4 | border-bottom: 1px solid #EEEEEE; 5 | } 6 | 7 | /************************************************************************************************/ 8 | 9 | .pagePieTable { 10 | margin: 7px; 11 | border-right: 1px solid #EEEEEE; 12 | padding-right: 7px; 13 | display: inline-table; 14 | } 15 | 16 | .pieGraph { 17 | width: 100px; 18 | height: 100px; 19 | display: block; 20 | } 21 | 22 | .pieLabel { 23 | font-size: 11px; 24 | padding-left: 10px; 25 | cursor: default; 26 | } 27 | 28 | .pieLabel SPAN { 29 | vertical-align: middle; 30 | } 31 | 32 | .pieLabel .box { 33 | display: inline-block; 34 | width: 10px; 35 | height: 10px; 36 | margin-top: 1px; 37 | } 38 | 39 | .pieLabel .label { 40 | padding-left: 5px; 41 | } 42 | 43 | .TimingPie.Blocked { color: rgb(228, 214, 193) } 44 | .TimingPie.DNS { color: rgb(119, 192, 203) } 45 | .TimingPie.SSL { color: rgb(168, 196, 173) } 46 | .TimingPie.Connect { color: rgb(179, 222, 93) } 47 | .TimingPie.Send { color: rgb(224, 171, 157) } 48 | .TimingPie.Wait { color: rgb(163, 150, 190) } 49 | .TimingPie.Receive { color: rgb(194, 194, 194) } 50 | 51 | .ContentPie.HTMLText { color: rgb(174, 234, 218) } 52 | .ContentPie.JavaScript { color: rgb(245, 230, 186) } 53 | .ContentPie.CSS { color: rgb(212, 204, 219) } 54 | .ContentPie.Image { color: rgb(220, 171, 181) } 55 | .ContentPie.Flash { color: rgb(166, 156, 222) } 56 | .ContentPie.Others { color: rgb(229, 171, 255) } 57 | 58 | .TrafficPie.HeadersSent { color: rgb(247, 179, 227) } 59 | .TrafficPie.BodiesSent { color: rgb(226, 160, 241) } 60 | .TrafficPie.HeadersReceived { color: rgb(166, 232, 166) } 61 | .TrafficPie.BodiesReceived { color: rgb(168, 196, 173) } 62 | 63 | .CachePie.Downloaded { color: rgb(182, 182, 182) } 64 | .CachePie.Partial { color: rgb(218, 218, 218) } 65 | .CachePie.FromCache { color: rgb(239, 239, 239) } 66 | -------------------------------------------------------------------------------- /webapp/css/harView.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .harBody { 4 | margin: 0; 5 | font-family: Lucida Grande, Tahoma, sans-serif; 6 | font-size: 13px; 7 | } 8 | 9 | #content a { 10 | text-decoration: none; 11 | } 12 | 13 | #content h1 { 14 | font-size: 17px; 15 | border-bottom: 1px solid threedlightshadow; 16 | } 17 | 18 | #content h2, #content h3, #content h4 { 19 | color: #DD467B; 20 | } 21 | 22 | #content h2 { 23 | font-size: 22.8px; 24 | font-weight: lighter; 25 | } 26 | 27 | #content h3 { 28 | font-weight: bold; 29 | } 30 | 31 | #content h4 { 32 | font-weight: bold; 33 | font-style: italic; 34 | } 35 | 36 | #content pre { 37 | margin: 0; 38 | font: inherit; 39 | } 40 | 41 | /*************************************************************************************************/ 42 | 43 | .collapsed, 44 | [collapsed="true"] { 45 | display: none !important; 46 | } 47 | 48 | .link { 49 | color: blue; 50 | } 51 | 52 | .link:hover { 53 | cursor: pointer; 54 | } 55 | 56 | /*************************************************************************************************/ 57 | 58 | .harViewBodies { 59 | position: absolute; 60 | top: 33px; 61 | bottom: 0px; 62 | } 63 | 64 | .harViewBar > .tab { 65 | font-size: 17px; 66 | font-family: Lucida Grande, Tahoma, sans-serif; 67 | } 68 | 69 | /*************************************************************************************************/ 70 | 71 | /* Support for hiding the tabBar */ 72 | .harView[hideTabBar="true"] .harViewBar { 73 | display: none; 74 | } 75 | 76 | .harView[hideTabBar="true"] .harViewBodies { 77 | position: inherit; 78 | } 79 | -------------------------------------------------------------------------------- /webapp/css/harViewer.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | @import url("harView.css"); 4 | 5 | /* Domplate Widgets */ 6 | @import url("tabView.css"); 7 | @import url("toolbar.css"); 8 | @import url("domTree.css"); 9 | @import url("infoTip.css"); 10 | @import url("popupMenu.css"); 11 | @import url("tableView.css"); 12 | 13 | /* HAR Preview */ 14 | @import url("pageList.css"); 15 | @import url("requestList.css"); 16 | @import url("requestBody.css"); 17 | 18 | /* @import url("previewMenu.css"); -- preview mode only */ 19 | 20 | @import url("harStats.css"); 21 | @import url("pageTimeline.css"); 22 | 23 | @import url("validationError.css"); 24 | 25 | /* Application Tabs */ 26 | @import url("aboutTab.css"); 27 | @import url("homeTab.css"); 28 | @import url("domTab.css"); 29 | @import url("schemaTab.css"); 30 | @import url("previewTab.css"); 31 | @import url("search.css"); 32 | 33 | /* highlight.js */ 34 | @import url("highlightjs/tomorrow.min.css"); 35 | 36 | @import url("dragdrop.css"); 37 | -------------------------------------------------------------------------------- /webapp/css/highlightjs/default.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;background:#F0F0F0}.hljs,.hljs-subst{color:#444}.hljs-comment{color:#888888}.hljs-keyword,.hljs-attribute,.hljs-selector-tag,.hljs-meta-keyword,.hljs-doctag,.hljs-name{font-weight:bold}.hljs-type,.hljs-string,.hljs-number,.hljs-selector-id,.hljs-selector-class,.hljs-quote,.hljs-template-tag,.hljs-deletion{color:#880000}.hljs-title,.hljs-section{color:#880000;font-weight:bold}.hljs-regexp,.hljs-symbol,.hljs-variable,.hljs-template-variable,.hljs-link,.hljs-selector-attr,.hljs-selector-pseudo{color:#BC6060}.hljs-literal{color:#78A960}.hljs-built_in,.hljs-bullet,.hljs-code,.hljs-addition{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta-string{color:#4d99bf}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /webapp/css/highlightjs/tomorrow.min.css: -------------------------------------------------------------------------------- 1 | .hljs-comment,.hljs-quote{color:#8e908c}.hljs-variable,.hljs-template-variable,.hljs-tag,.hljs-name,.hljs-selector-id,.hljs-selector-class,.hljs-regexp,.hljs-deletion{color:#c82829}.hljs-number,.hljs-built_in,.hljs-builtin-name,.hljs-literal,.hljs-type,.hljs-params,.hljs-meta,.hljs-link{color:#f5871f}.hljs-attribute{color:#eab700}.hljs-string,.hljs-symbol,.hljs-bullet,.hljs-addition{color:#718c00}.hljs-title,.hljs-section{color:#4271ae}.hljs-keyword,.hljs-selector-tag{color:#8959a8}.hljs{display:block;overflow-x:auto;background:white;color:#4d4d4c;padding:0.5em}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /webapp/css/homeTab.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .tabHomeBody { 4 | font-family: Verdana,Geneva,Arial,Helvetica,sans-serif; 5 | font-size: 11.7px; 6 | font-style: normal; 7 | font-weight: 400; 8 | } 9 | 10 | .homeBody { 11 | padding: 8px; 12 | } 13 | -------------------------------------------------------------------------------- /webapp/css/images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/ajax-loader.gif -------------------------------------------------------------------------------- /webapp/css/images/bg-button.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/bg-button.gif -------------------------------------------------------------------------------- /webapp/css/images/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/blank.gif -------------------------------------------------------------------------------- /webapp/css/images/button-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/button-background.png -------------------------------------------------------------------------------- /webapp/css/images/checkmark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/checkmark.gif -------------------------------------------------------------------------------- /webapp/css/images/checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/checkmark.png -------------------------------------------------------------------------------- /webapp/css/images/close-sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/close-sprites.png -------------------------------------------------------------------------------- /webapp/css/images/contextMenuTarget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/contextMenuTarget.png -------------------------------------------------------------------------------- /webapp/css/images/contextMenuTargetHover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/contextMenuTargetHover.png -------------------------------------------------------------------------------- /webapp/css/images/download-sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/download-sprites.png -------------------------------------------------------------------------------- /webapp/css/images/downloadButtons-aero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/downloadButtons-aero.png -------------------------------------------------------------------------------- /webapp/css/images/group.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/group.gif -------------------------------------------------------------------------------- /webapp/css/images/loading_16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/loading_16.gif -------------------------------------------------------------------------------- /webapp/css/images/menu/previewMenuHandle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/menu/previewMenuHandle.png -------------------------------------------------------------------------------- /webapp/css/images/menu/shadowAlpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/menu/shadowAlpha.png -------------------------------------------------------------------------------- /webapp/css/images/menu/tabMenuCheckbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/menu/tabMenuCheckbox.png -------------------------------------------------------------------------------- /webapp/css/images/menu/tabMenuPin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/menu/tabMenuPin.png -------------------------------------------------------------------------------- /webapp/css/images/menu/tabMenuRadio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/menu/tabMenuRadio.png -------------------------------------------------------------------------------- /webapp/css/images/netBarBlocking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarBlocking.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarBlocking2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarBlocking2.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarCached.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarCached.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarConnecting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarConnecting.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarLoaded.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarLoaded.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarReceiving.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarReceiving.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarResolving.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarResolving.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarResponded.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarResponded.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarSending.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarSending.gif -------------------------------------------------------------------------------- /webapp/css/images/netBarWaiting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/netBarWaiting.gif -------------------------------------------------------------------------------- /webapp/css/images/page-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/page-timeline.png -------------------------------------------------------------------------------- /webapp/css/images/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/save.png -------------------------------------------------------------------------------- /webapp/css/images/splitterh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/splitterh.png -------------------------------------------------------------------------------- /webapp/css/images/spriteArrows.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/spriteArrows.gif -------------------------------------------------------------------------------- /webapp/css/images/spriteArrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/spriteArrows.png -------------------------------------------------------------------------------- /webapp/css/images/tabEnabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/tabEnabled.png -------------------------------------------------------------------------------- /webapp/css/images/timeline-sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/timeline-sprites.png -------------------------------------------------------------------------------- /webapp/css/images/tooltipConnectorUp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/tooltipConnectorUp.png -------------------------------------------------------------------------------- /webapp/css/images/twisty-sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitgrimbo/harviewer-react/f9dd622074bf0ce3f8a9f907adf2659a957ade9b/webapp/css/images/twisty-sprites.png -------------------------------------------------------------------------------- /webapp/css/infoTip.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .infoTip { 4 | z-index: 2147483647; 5 | position: fixed; 6 | padding: 2px 4px 3px 4px; 7 | background: LightYellow; 8 | font-family: Lucida Grande, Tahoma, sans-serif; 9 | color: #000000; 10 | display: none; 11 | white-space: nowrap; 12 | font-size: 11px; 13 | border: 1px solid rgb(126, 171, 205); 14 | background: url(images/tabEnabled.png) repeat-x scroll 0px 0px rgb(249, 249, 249); 15 | background-position-x: 0; 16 | background-position-y: 100%; 17 | background-repeat: repeat-x; 18 | 19 | -moz-border-radius: 3px; 20 | -webkit-border-radius: 3px; 21 | border-radius: 3px; 22 | 23 | -moz-box-shadow: gray 2px 2px 3px; 24 | -webkit-box-shadow: gray 2px 2px 3px; 25 | box-shadow: gray 2px 2px 3px; 26 | 27 | /* IE */ 28 | filter:progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray'); 29 | /* slightly different syntax for IE8 */ 30 | -ms-filter:"progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray')"; 31 | } 32 | 33 | .infoTip[active="true"] { 34 | display: block; 35 | } 36 | 37 | .infoTip[multiline="true"] { 38 | background-image: none; 39 | } 40 | -------------------------------------------------------------------------------- /webapp/css/pageList.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .pageList { 4 | width: 100%; 5 | } 6 | 7 | .pageTable { 8 | width: 100%; 9 | font-family: Lucida Grande, Tahoma, sans-serif; 10 | font-size: 11px; 11 | } 12 | 13 | .pageCol { 14 | white-space: nowrap; 15 | border-bottom: 1px solid #EEEEEE; 16 | } 17 | 18 | .pageRow { 19 | font-weight: bold; 20 | height: 17px; 21 | background-color: white; 22 | } 23 | 24 | .pageRow:hover { 25 | background: #EFEFEF; 26 | } 27 | 28 | .opened > .pageCol > .pageName { 29 | background-image: url(images/twisty-sprites.png); 30 | background-position: 3px -17px; 31 | } 32 | 33 | .pageName { 34 | background-image: url(images/twisty-sprites.png); 35 | background-repeat: no-repeat; 36 | background-position: 3px 2px; 37 | padding-left: 18px; 38 | font-weight: bold; 39 | cursor:pointer; 40 | } 41 | 42 | .pageID { 43 | color: gray; 44 | } 45 | 46 | .pageInfoCol { 47 | background: url(images/timeline-sprites.png) repeat-x scroll 0 -112px #FFFFFF; 48 | padding: 0px 0px 4px 17px; 49 | } 50 | 51 | /*************************************************************************************************/ 52 | /* Column Customization */ 53 | 54 | .pageRow:hover > .netOptionsCol > .netOptionsLabel { 55 | display: block; 56 | } 57 | 58 | .pageRow > .netOptionsCol { 59 | padding-right: 2px; 60 | } 61 | 62 | /*************************************************************************************************/ 63 | /* Print support */ 64 | 65 | @media print { 66 | 67 | .pageInfoCol { 68 | background: none; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /webapp/css/pageTimeline.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .pageTimeline { 4 | background-color: #FFFFFF; 5 | color: #000000; 6 | } 7 | 8 | .pageTimelineBody { 9 | width: 100%; 10 | } 11 | 12 | /* If the page timeline is displayed there is a bottom line to separate 13 | * the list of pages/requests. */ 14 | .pageTimelineBody[class~=opened] { 15 | border-bottom: 1px solid #EEEEEE; 16 | } 17 | 18 | .pageTimelineTable { 19 | height: 100px; 20 | padding-left: 5px; 21 | } 22 | 23 | .pageTimelineCol { 24 | vertical-align: bottom; 25 | padding-left: 4px; 26 | outline: none; 27 | -moz-outline-style: none; 28 | -moz-user-focus: ignore; 29 | } 30 | 31 | .pageBar { 32 | width: 9px; 33 | background: url(images/page-timeline.png) repeat-y scroll 0px 0px #FFFFFF; 34 | cursor: pointer; 35 | } 36 | 37 | .pageBar.selected { 38 | opacity: 0.5; /* Safari, Opera */ 39 | -moz-opacity: 0.50; /* FireFox */ 40 | filter: alpha(opacity=50); /* IE */ 41 | } 42 | 43 | .pageTimelineCol:hover .pageBar { 44 | background: url(images/page-timeline.png) repeat-y scroll -8px 0px #FFFFFF; 45 | } 46 | 47 | .pageTimelineBody .connector { 48 | margin-left: 16px; 49 | display: block; 50 | background: url(images/tooltipConnectorUp.png) no-repeat; 51 | width: 16px; 52 | height: 11px; 53 | position: relative; 54 | margin-bottom: -1px; 55 | } 56 | 57 | /************************************************************************************************/ 58 | 59 | .pageDescBox .desc { 60 | font-size: 11px; 61 | border: 1px solid rgb(126, 171, 205); 62 | background: url(images/tabEnabled.png) repeat-x scroll 0px 0px rgb(234, 234, 234); 63 | padding: 3px; 64 | -moz-border-radius: 3px; 65 | -webkit-border-radius: 3px; 66 | border-radius: 3px; 67 | } 68 | 69 | .pageDescBox .desc .summary { 70 | font-weight: bold; 71 | } 72 | 73 | .pageDescBox .desc .time { 74 | padding-left: 10px; 75 | } 76 | 77 | .pageDescBox .desc .title { 78 | color: black; 79 | padding-left: 10px; 80 | } 81 | 82 | .pageDescBox .desc .comment { 83 | color: gray; 84 | padding-top: 1px; 85 | } 86 | -------------------------------------------------------------------------------- /webapp/css/popupMenu.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .popupMenu { 4 | display: none; 5 | position: absolute; 6 | font-size: 11px; 7 | z-index: 2147483647; 8 | font-family: Lucida Grande, Tahoma, sans-serif; 9 | } 10 | 11 | .popupMenuContent { 12 | padding: 2px; 13 | } 14 | 15 | .popupMenuSeparator { 16 | display: block; 17 | position: relative; 18 | padding: 1px 18px 0; 19 | text-decoration: none; 20 | color: #000; 21 | cursor: default; 22 | background: #ACA899; 23 | margin: 2px 0; 24 | } 25 | 26 | .popupMenuOption 27 | { 28 | display: block; 29 | position: relative; 30 | padding: 2px 18px; 31 | text-decoration: none; 32 | color: #000; 33 | cursor: default; 34 | } 35 | 36 | .popupMenuOption:hover 37 | { 38 | color: #fff; 39 | background: #316AC5; 40 | } 41 | 42 | .popupMenuGroup { 43 | background: transparent url(images/menu/tabMenuPin.png) no-repeat right 0; 44 | } 45 | 46 | .popupMenuGroup:hover { 47 | background: #316AC5 url(images/menu/tabMenuPin.png) no-repeat right -17px; 48 | } 49 | 50 | .popupMenuGroupSelected { 51 | color: #fff; 52 | background: #316AC5 url(images/menu/tabMenuPin.png) no-repeat right -17px; 53 | } 54 | 55 | .popupMenuChecked { 56 | background: transparent url(images/menu/tabMenuCheckbox.png) no-repeat 4px 0; 57 | } 58 | 59 | .popupMenuChecked:hover { 60 | background: #316AC5 url(images/menu/tabMenuCheckbox.png) no-repeat 4px -17px; 61 | } 62 | 63 | .popupMenuRadioSelected { 64 | background: transparent url(images/menu/tabMenuRadio.png) no-repeat 4px 0; 65 | } 66 | 67 | .popupMenuRadioSelected:hover { 68 | background: #316AC5 url(images/menu/tabMenuRadio.png) no-repeat 4px -17px; 69 | } 70 | 71 | .popupMenuShortcut { 72 | padding-right: 85px; 73 | } 74 | 75 | .popupMenuShortcutKey { 76 | position: absolute; 77 | right: 0; 78 | top: 2px; 79 | width: 77px; 80 | } 81 | 82 | .popupMenuDisabled { 83 | color: #ACA899 !important; 84 | } 85 | 86 | /*************************************************************************************************/ 87 | /* Shadow */ 88 | 89 | .popupMenuShadow { 90 | float: left; 91 | background: url(images/menu/shadowAlpha.png) no-repeat bottom right !important; 92 | margin: 10px 0 0 10px !important; 93 | margin: 10px 0 0 5px; 94 | } 95 | 96 | .popupMenuShadowContent { 97 | display: block; 98 | position: relative; 99 | background-color: #fff; 100 | border: 1px solid #a9a9a9; 101 | top: -6px; 102 | left: -6px; 103 | } 104 | 105 | /*************************************************************************************************/ 106 | 107 | #optionsMenu { /*xxxHonza*/ 108 | top: 22px; 109 | left: 0; 110 | } 111 | 112 | -------------------------------------------------------------------------------- /webapp/css/previewMenu.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .menu { 4 | position: absolute; 5 | right: 2px; 6 | font-size: 10px; 7 | font-family: Lucida Grande,Tahoma,sans-serif; 8 | text-decoration: none; 9 | outline: none; 10 | white-space: nowrap; 11 | } 12 | 13 | .menu .menuContent { 14 | display: inline-block; 15 | overflow: hidden; 16 | vertical-align: top; 17 | line-height: 13px; 18 | } 19 | 20 | .menu .menuHandle { 21 | display: inline-block; 22 | width: 9px; 23 | height: 16px; 24 | background-image: url('images/menu/previewMenuHandle.png'); 25 | } 26 | 27 | .menu .menuHandle.opened { 28 | background-image: url('images/menu/previewMenuHandle.png'); 29 | background-position: 9px 0; 30 | } 31 | 32 | /*************************************************************************************************/ 33 | /* Toolbar inside the menu */ 34 | 35 | .menu .toolbar { 36 | border: 0; 37 | vertical-align: top; 38 | display: inline; 39 | padding-left: 0; 40 | } 41 | 42 | .menu .toolbarSeparator { 43 | } 44 | 45 | .menu .toolbarSeparator, 46 | .menu .toolbarButton { 47 | padding: 0; 48 | margin: 0 3px 1px 3px; 49 | border: none; 50 | color: gray; 51 | } 52 | 53 | .toolbarButton.text:hover { 54 | border: none; 55 | background: none; 56 | color: blue; 57 | } 58 | -------------------------------------------------------------------------------- /webapp/css/previewTab.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /*************************************************************************************************/ 4 | /* Preview */ 5 | 6 | /*************************************************************************************************/ 7 | /* Toolbar */ 8 | 9 | /* Remove the dotted outline when focused */ 10 | .harDownloadButton OBJECT { 11 | outline: none; 12 | -moz-user-focus: ignore; 13 | } 14 | 15 | .harSaveButton { 16 | background: url(images/save.png) no-repeat; 17 | } 18 | -------------------------------------------------------------------------------- /webapp/css/requestBody.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .requestBodyBodies { 4 | border-left: 1px solid #D7D7D7; 5 | border-right: 1px solid #D7D7D7; 6 | border-bottom: 1px solid #D7D7D7; 7 | padding-top: 8px; 8 | } 9 | 10 | .netInfoRow .tabView { 11 | width: 99%; /* avoid 1px horizontal scrollbar when a requst is expanded and tabView visible */ 12 | } 13 | 14 | .netInfoText { 15 | padding: 8px; 16 | background-color: #FFFFFF; 17 | font-family: Monaco, monospace; 18 | /*overflow-x: auto; HTML is damaged in case of big (2-3MB) responses */ 19 | } 20 | 21 | .netInfoRequestURL { 22 | padding: 8px; 23 | } 24 | 25 | .netInfoText[selected="true"] { 26 | display: block; 27 | } 28 | 29 | /*************************************************************************************************/ 30 | 31 | .netInfoParamName { 32 | padding: 0 10px 0 0; 33 | font-family: Lucida Grande, Tahoma, sans-serif; 34 | font-weight: bold; 35 | vertical-align: top; 36 | text-align: right; 37 | white-space: nowrap; 38 | } 39 | 40 | .netInfoParamValue > PRE { 41 | margin: 0 42 | } 43 | 44 | .netInfoHeadersText, 45 | .netInfoCookiesText { 46 | padding-top: 0; 47 | width: 100%; 48 | } 49 | 50 | .netInfoParamValue { 51 | width: 100%; 52 | } 53 | 54 | .netInfoHeadersGroup, 55 | .netInfoCookiesGroup { 56 | margin-bottom: 4px; 57 | border-bottom: 1px solid #D7D7D7; 58 | padding-top: 8px; 59 | padding-bottom: 2px; 60 | font-family: Lucida Grande, Tahoma, sans-serif; 61 | font-weight: bold; 62 | color: #565656; 63 | } 64 | 65 | /*************************************************************************************************/ 66 | /* HTML Tab */ 67 | 68 | .netInfoHtmlPreview { 69 | border: 0; 70 | width: 100%; 71 | height: 100px; 72 | } 73 | 74 | .netInfoHtmlText { 75 | padding: 0; 76 | } 77 | 78 | /* Preview resizer */ 79 | .htmlPreviewResizer { 80 | width: 100%; 81 | height: 4px; 82 | background-image: url(images/splitterh.png); 83 | background-repeat: repeat-x; 84 | cursor: s-resize; 85 | } 86 | 87 | /* When HTML preview resizing is in progress mouse messages are not sent to 88 | the iframe document. */ 89 | body[hResizing="true"] .netInfoHtmlPreview { 90 | pointer-events: none !important; 91 | } 92 | -------------------------------------------------------------------------------- /webapp/css/schemaTab.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .tabSchemaBody { 4 | font-family: Monaco,monospace; 5 | font-size: 12px; 6 | } 7 | -------------------------------------------------------------------------------- /webapp/css/search.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /*************************************************************************************************/ 4 | /* Search */ 5 | 6 | .searchTextBox { 7 | vertical-align: sub; 8 | margin-right: 3px; 9 | } 10 | 11 | .searchInput { 12 | outline: none; /* Remove webkit focus border */ 13 | border: 1px solid #EEEEEE; 14 | padding: 1px 17px 1px 3px; 15 | width: 300px; 16 | } 17 | 18 | .searchInput[status=notfound] { 19 | background: rgb(255, 102, 102); 20 | color: white; 21 | } 22 | 23 | .searchBox .arrow { 24 | width: 11px; 25 | height: 10px; 26 | background: url(images/contextMenuTarget.png) no-repeat; 27 | display: inline-block; 28 | position: relative; 29 | margin-left: -15px; 30 | top: 1px; 31 | cursor: pointer; 32 | 33 | } 34 | 35 | .searchBox .arrow:hover { 36 | background-image: url(images/contextMenuTargetHover.png); 37 | } 38 | 39 | /* Disabled for IE */ 40 | .searchBox .arrow[disabled=true] { 41 | display: none; 42 | } 43 | 44 | .searchBox .resizer { 45 | cursor: e-resize; 46 | } 47 | -------------------------------------------------------------------------------- /webapp/css/tabView.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .tabView { 4 | width: 100%; 5 | background-color: #FFFFFF; 6 | color: #000000; 7 | } 8 | 9 | .tabViewCol { 10 | background: url(images/timeline-sprites.png) repeat-x scroll 0 -112px #FFFFFF; 11 | vertical-align:top; 12 | } 13 | 14 | .tabViewBody { 15 | margin: 2px 0px 0px 0px; 16 | } 17 | 18 | .tabBar { 19 | padding-left: 14px; 20 | border-bottom: 1px solid #D7D7D7; 21 | white-space: nowrap; 22 | } 23 | 24 | .tab { 25 | position: relative; 26 | top: 1px; 27 | padding: 4px 8px; 28 | border: 1px solid transparent; 29 | border-bottom: none; 30 | color: #565656; 31 | font-weight: bold; 32 | white-space: nowrap; 33 | -moz-user-select: none; 34 | display:inline-block; 35 | } 36 | 37 | .tab:hover { 38 | cursor: pointer; 39 | border-color: #D7D7D7; 40 | -moz-border-radius: 4px 4px 0 0; 41 | -webkit-border-top-left-radius: 4px; 42 | -webkit-border-top-right-radius: 4px; 43 | border-radius: 4px 4px 0 0; 44 | } 45 | 46 | .tab[selected="true"], 47 | .tab .selected { 48 | cursor: default !important; 49 | border-color: #D7D7D7; 50 | background-color: #FFFFFF; 51 | -moz-border-radius: 4px 4px 0 0; 52 | -webkit-border-top-left-radius: 4px; 53 | -webkit-border-top-right-radius: 4px; 54 | border-radius: 4px 4px 0 0; 55 | } 56 | 57 | /* 58 | * If fixed height is specified, the overflow works, otherwise the position must be 59 | * absolute with top: 33px (height of the tab-header and bottom: 0px to get available 60 | * vertical space 61 | */ 62 | .tabBodies { 63 | width: 100%; 64 | overflow: auto; 65 | } 66 | 67 | .tabBody { 68 | display: none; 69 | margin: 0; 70 | } 71 | 72 | .tabBody[selected="true"], 73 | .tabBody.selected { 74 | display: block; 75 | } 76 | 77 | /*************************************************************************************************/ 78 | /* Print support */ 79 | 80 | @media print { 81 | 82 | /* This is what prevents the browser's print featurs to print more pages. */ 83 | .tabBodies { 84 | overflow:visible; 85 | } 86 | 87 | .tabViewCol { 88 | background: none; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /webapp/css/tableView.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /*************************************************************************************************/ 4 | /* Console Panel */ 5 | 6 | .dataTableSizer { 7 | margin: 7px; 8 | border: 1px solid #EEEEEE; 9 | } 10 | 11 | .dataTableSizer:focus { 12 | outline: none; 13 | } 14 | 15 | .dataTable { 16 | } 17 | 18 | /* If the table is displayed within a group the border is provided by that group. */ 19 | .logGroup .dataTable { 20 | border: none; 21 | } 22 | 23 | .dataTable TBODY { 24 | overflow-x: hidden; 25 | overflow-y: scroll; 26 | } 27 | 28 | .dataTable > .dataTableTbody > tr:nth-child(even) { 29 | background-color: #EFEFEF; 30 | } 31 | 32 | .dataTable a { 33 | vertical-align:middle; 34 | } 35 | 36 | .dataTableTbody > tr > td { 37 | padding: 1px 4px 0 4px; 38 | } 39 | 40 | /* The last column needs more horizontal space since part of it is overlapped by 41 | the vertical scroll bar */ 42 | .dataTableTbody > tr > td:last-child { 43 | padding-right: 20px; 44 | } 45 | 46 | .useA11y .dataTable *:focus { 47 | outline-offset: -2px; 48 | } 49 | 50 | /*************************************************************************************************/ 51 | /* Console panel filter */ 52 | 53 | .panelNode.hideType-table .logRow-table { 54 | display: none !important; 55 | } 56 | 57 | /*************************************************************************************************/ 58 | /* Header for Net panel table */ 59 | 60 | .headerCell { 61 | cursor: pointer; 62 | -moz-user-select: none; 63 | border-bottom: 1px solid #9C9C9C; 64 | padding: 0 !important; 65 | font-weight: bold; 66 | background: #C8C8C8 -moz-linear-gradient(top, rgba(255, 255, 255, 0.3), rgba(0, 0, 0, 0.2)); 67 | background: #C8C8C8 -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 0.3)), to(rgba(0, 0, 0, 0.2))); 68 | } 69 | 70 | .headerCellBox { 71 | padding: 2px 13px 2px 4px; 72 | border-left: 1px solid #D9D9D9; 73 | border-right: 1px solid #9C9C9C; 74 | white-space: nowrap; 75 | } 76 | 77 | .headerCell:hover:active { 78 | background-color: #B4B4B4; 79 | } 80 | 81 | .headerSorted { 82 | background-color: #8CA0BE; 83 | } 84 | 85 | .headerSorted > .headerCellBox { 86 | border-right-color: #6B7C93; 87 | background: url(chrome://firebug/skin/arrowDown.png) no-repeat right; 88 | } 89 | 90 | .headerSorted.sortedAscending > .headerCellBox { 91 | background-image: url(chrome://firebug/skin/arrowUp.png); 92 | } 93 | 94 | .headerSorted:hover:active { 95 | background-color: #6E87AA; 96 | } 97 | 98 | /*************************************************************************************************/ 99 | /* Tree within a table cell */ 100 | 101 | /* Rules for the (fake) root object that represents an expandable tree within a table-cell */ 102 | .memberRow.tableCellRow .memberLabelCell, 103 | .memberRow.tableCellRow .memberValueCell { 104 | padding: 0; 105 | color: Gray; 106 | } 107 | 108 | /* So the height of the row is the same as if there would be an embedded tree object */ 109 | .dataTableCell > .objectBox-number, 110 | .dataTableCell > .objectBox-string, 111 | .dataTableCell > .objectBox-null, 112 | .dataTableCell > .objectBox-undefined, 113 | .dataTableCell > .objectBox-array { 114 | padding: 5px; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /webapp/css/toolTip.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .toolTip { 4 | z-index: 2147483647; 5 | position: absolute; 6 | padding: 2px 4px 3px 4px; 7 | font-family: Lucida Grande, Tahoma, sans-serif; 8 | color: #000000; 9 | display: none; 10 | font-size: 11px; 11 | border: 1px solid #a9a9a9; 12 | background: white; 13 | overflow: auto; 14 | } 15 | 16 | .toolTip { 17 | height: 150px; 18 | width: 500px; 19 | } 20 | -------------------------------------------------------------------------------- /webapp/css/toolbar.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | .toolbar { 4 | font-family: Verdana,Geneva,Arial,Helvetica,sans-serif; 5 | font-size: 11px; 6 | font-weight: 400; 7 | font-style: normal; 8 | border-bottom: 1px solid #EEEEEE; 9 | padding: 0 3px 0 3px; 10 | } 11 | 12 | .toolbarButton, 13 | .toolbarSeparator { 14 | display: inline-block; 15 | vertical-align: middle; 16 | cursor: pointer; 17 | color: #000000; 18 | -moz-user-select: none; 19 | -moz-box-sizing: padding-box; 20 | } 21 | 22 | .toolbarButton.dropDown .arrow { 23 | width: 11px; 24 | height: 10px; 25 | background: url(images/contextMenuTarget.png) no-repeat; 26 | display: inline-block; 27 | margin-left: 3px; 28 | position: relative; 29 | right: 0; 30 | top: 1px; 31 | } 32 | 33 | .toolbarButton.image { 34 | padding: 0; 35 | height: 16px; 36 | width: 16px; 37 | } 38 | 39 | .toolbarButton.text, 40 | .toolbarSeparator { 41 | margin: 3px 0 3px 0; 42 | padding: 3px; 43 | border: 1px solid transparent; 44 | } 45 | 46 | .toolbarButton.text:hover { 47 | background: url(images/bg-button.gif) repeat-x scroll 0 0 #FFFFFF; 48 | border-top:1px solid #bbb; 49 | border-bottom:1px solid #aaa; 50 | border-left:1px solid #bbb; 51 | border-right:1px solid #aaa; 52 | -moz-border-radius: 3px; 53 | -webkit-border-radius: 3px; 54 | border-radius: 3px; 55 | } 56 | 57 | .toolbarButton.text:active { 58 | background-position: 0 -400px; 59 | } 60 | -------------------------------------------------------------------------------- /webapp/css/validationError.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /*************************************************************************************************/ 4 | /* Errors */ 5 | 6 | .errorTable { 7 | margin: 8px; 8 | font-size: 13px; 9 | } 10 | 11 | .errorProperty { 12 | color: gray; 13 | font-style:italic; 14 | } 15 | 16 | .errorMessage { 17 | color: red; 18 | } 19 | 20 | .errorRow:hover { 21 | background: #EFEFEF; 22 | cursor: pointer; 23 | } 24 | 25 | /*************************************************************************************************/ 26 | /* Context Menu */ 27 | 28 | .errorOptionsTarget { 29 | width: 11px; 30 | height: 10px; 31 | background: url(images/contextMenuTarget.png) no-repeat; 32 | visibility: collapse; 33 | } 34 | 35 | .errorOptionsTarget:hover { 36 | background-image: url(images/contextMenuTargetHover.png); 37 | } 38 | 39 | /** 40 | * The context menu target is visible only if the user is hovering mouse over 41 | * the error entry and if the error is associated with a target HAR object 42 | */ 43 | .errorRow:hover > .errorOptions.hasTarget > .errorOptionsTarget { 44 | visibility: visible; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /webapp/css/xhrSpy.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | @import url("tabView.css"); 4 | @import url("pageList.css"); 5 | @import url("requestList.css"); 6 | @import url("requestBody.css"); 7 | @import url("infoTip.css"); 8 | @import url("popupMenu.css"); 9 | @import url("toolbar.css"); 10 | 11 | .harBody { 12 | margin: 0; 13 | font-family: Lucida Grande, Tahoma, sans-serif; 14 | font-size: 11px; 15 | } 16 | 17 | .xhrSpyFrame { 18 | } 19 | 20 | .tabView.requestBody, 21 | .netInfoHeadersGroup { 22 | /* font-size: 11px;*/ 23 | } 24 | -------------------------------------------------------------------------------- /webapp/demo.js: -------------------------------------------------------------------------------- 1 | import "@babel/polyfill"; 2 | import React from "react"; 3 | import { render } from "react-dom"; 4 | 5 | import HarModel from "./modules/preview/harModel"; 6 | import HarModelLoader from "./modules/preview/harModelLoader"; 7 | 8 | import DemoApp from "./modules/Demo"; 9 | 10 | function loadHar(harUrl, callback, errback) { 11 | harUrl = harUrl || "../examples/softwareishard.com.har"; 12 | HarModelLoader.loadArchives([harUrl], [], null, callback); 13 | } 14 | 15 | window.addEventListener("load", function() { 16 | function renderDemo(demoName, demoProps) { 17 | render( 18 | , document.getElementById("demo-app") 19 | ); 20 | } 21 | 22 | function createDemoProps(har) { 23 | const model = new HarModel(); 24 | model.append(har); 25 | const firstPage = har.log.pages[0]; 26 | const firstPageEntries = model.getPageEntries(firstPage); 27 | const firstEntryOfFirstPage = firstPageEntries[0]; 28 | return { 29 | model, 30 | input: har, 31 | page: firstPage, 32 | entries: firstPageEntries, 33 | entry: firstEntryOfFirstPage, 34 | visible: true, 35 | }; 36 | } 37 | 38 | // eslint-disable-next-line 39 | const uri = URI(window.location.href); 40 | const params = uri.query(true); 41 | if (params.demo) { 42 | loadHar(null, (har) => renderDemo(params.demo, createDemoProps(har))); 43 | } else { 44 | renderDemo(); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /webapp/demos/demo-shell.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /webapp/examples/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTTP Archive Viewer @VERSION@ - Embedding Samples 6 | 7 | 10 | 11 | 12 | 13 | 14 |

View source to see examples of how to embed HAR Viewer into your page.

15 | 16 |

17 | Embed as 18 | or 19 | with height 20 | 21 | 22 | 23 |

24 | 25 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 |

HAR/HTML

102 |
103 |
104 | 105 |
106 |

HARP/HTML

107 |
108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTTP Archive Viewer @VERSION@ 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 28 | 29 | 32 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /webapp/loader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HAR Viewer - Service Loader 5 | 6 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 42 | 43 |
38 | 39 |
40 | Full Preview 41 |
44 | 45 | 47 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /webapp/main.js: -------------------------------------------------------------------------------- 1 | import "@babel/polyfill"; 2 | import React from "react"; 3 | import { render } from "react-dom"; 4 | import App from "./modules/App"; 5 | import { AppContextProvider } from "./modules/AppContext"; 6 | 7 | const path = window.location.href.split("?")[0]; 8 | const mode = (path.endsWith("preview.html")) ? "preview" : ""; 9 | const container = document.getElementById("content"); 10 | render(( 11 | 12 | 13 | 14 | ), container); 15 | -------------------------------------------------------------------------------- /webapp/modules/AppContext.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import Url from "./core/url"; 4 | 5 | import Cookies from "./core/cookies"; 6 | import InfoTipHolder from "./InfoTipHolder"; 7 | import defaultTimingDefinitions from "./nettable/defaultTimingDefinitions"; 8 | 9 | const DEFAULT_STATE = { 10 | validate: true, 11 | expandAll: true, 12 | previewCols: [], 13 | pageTimingDefinitions: defaultTimingDefinitions.slice(), 14 | }; 15 | 16 | const AppContext = React.createContext(DEFAULT_STATE); 17 | 18 | export default AppContext; 19 | 20 | export const AppContextConsumer = AppContext.Consumer; 21 | 22 | export class AppContextProvider extends Component { 23 | state = DEFAULT_STATE; 24 | infoTipHolderRef = React.createRef(); 25 | 26 | getDefaultVisibleNetCols() { 27 | const cols = Cookies.getCookie("previewCols"); 28 | if (cols) { 29 | // Columns names are separated by a space so, make sure to properly process 30 | // spaces in the cookie value. 31 | return unescape(cols.replace(/\+/g, " ")) 32 | .split(" "); 33 | } 34 | const defaultVisibleNetCols = [ 35 | "url", 36 | "status", 37 | "size", 38 | "uncompressedSize", 39 | "timeline", 40 | ]; 41 | return defaultVisibleNetCols; 42 | } 43 | 44 | componentDidMount() { 45 | const expandAll = Url.getURLParameter("expand", window.location.href) === "true"; 46 | const validate = Cookies.getCookie("validate") !== "false"; 47 | const newState = { 48 | validate, 49 | expandAll, 50 | previewCols: this.getDefaultVisibleNetCols(), 51 | }; 52 | this.setState(newState); 53 | } 54 | 55 | appendPreview(harObjectOrString) { 56 | } 57 | 58 | setPreviewCols = (cols, avoidCookies) => { 59 | if (!cols) { 60 | cols = this.getDefaultVisibleNetCols(); 61 | } 62 | 63 | // If the parameter is an array, convert it to string. 64 | if (!Array.isArray(cols)) { 65 | cols = cols.split(/\s+/); 66 | } 67 | 68 | // Update cookie 69 | if (!avoidCookies) { 70 | Cookies.setCookie("previewCols", cols.join(" ")); 71 | } 72 | 73 | this.setState({ 74 | previewCols: cols, 75 | }); 76 | } 77 | 78 | setValidate = (validate) => { 79 | Cookies.setCookie("validate", validate); 80 | this.setState({ validate }); 81 | } 82 | 83 | getInfoTipHolder = () => { 84 | return this.infoTipHolderRef.current; 85 | } 86 | 87 | addPageTiming = (timing) => { 88 | this.setState(({ pageTimingDefinitions }) => ({ 89 | pageTimingDefinitions: [...pageTimingDefinitions, timing], 90 | })); 91 | } 92 | 93 | render() { 94 | const state = this.state; 95 | return ( 96 | 97 | 104 | {this.props.children} 105 | 106 | 107 | ); 108 | } 109 | } 110 | 111 | AppContextProvider.propTypes = { 112 | children: PropTypes.node, 113 | }; 114 | -------------------------------------------------------------------------------- /webapp/modules/InfoTip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class InfoTip extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | active: false, 9 | multiline: false, 10 | }; 11 | this.domRef = React.createRef(); 12 | } 13 | 14 | placeInfoTip(infoTip, x, y) { 15 | const { infoTipMargin, infoTipWindowPadding } = this.props; 16 | 17 | const docEl = infoTip.ownerDocument.documentElement; 18 | const panelWidth = docEl.clientWidth; 19 | const panelHeight = docEl.clientHeight; 20 | 21 | const style = {}; 22 | 23 | if (x + infoTip.offsetWidth + infoTipMargin > 24 | panelWidth - infoTipWindowPadding) { 25 | style.left = "auto"; 26 | style.right = ((panelWidth - x) + infoTipMargin) + "px"; 27 | } else { 28 | style.left = (x + infoTipMargin) + "px"; 29 | style.right = "auto"; 30 | } 31 | 32 | if (y + infoTip.offsetHeight + infoTipMargin > panelHeight) { 33 | style.top = Math.max(0, 34 | panelHeight - (infoTip.offsetHeight + infoTipMargin)) + "px"; 35 | style.bottom = "auto"; 36 | } else { 37 | style.top = (y + infoTipMargin) + "px"; 38 | style.bottom = "auto"; 39 | } 40 | 41 | infoTip.style.top = style.top; 42 | infoTip.style.left = style.left; 43 | infoTip.style.right = style.right; 44 | infoTip.style.bottom = style.bottom; 45 | } 46 | 47 | refresh(props, state) { 48 | const { active, multiline, x, y } = state; 49 | this.setActiveAttr(this.domRef.current, active, multiline); 50 | if (active) { 51 | this.placeInfoTip(this.domRef.current, x, y); 52 | } 53 | } 54 | 55 | setActiveAttr(ref, active, multiline) { 56 | ref.setAttribute("active", active); 57 | // There is no background image for mulitline tooltips. 58 | ref.setAttribute("multiline", multiline); 59 | } 60 | 61 | componentDidMount() { 62 | this.refresh(this.props, this.state); 63 | if (this.props.onRef) { 64 | this.props.onRef(this.domRef.current); 65 | } 66 | } 67 | 68 | UNSAFE_componentWillUpdate(nextProps, nextState) { 69 | this.refresh(nextProps, nextState); 70 | } 71 | 72 | render() { 73 | return ( 74 |
75 | {this.props.children} 76 |
77 | ); 78 | } 79 | } 80 | 81 | InfoTip.propTypes = { 82 | children: PropTypes.node, 83 | onRef: PropTypes.func, 84 | infoTipMargin: PropTypes.number, 85 | infoTipWindowPadding: PropTypes.number, 86 | }; 87 | 88 | export default InfoTip; 89 | -------------------------------------------------------------------------------- /webapp/modules/InfoTipHolder.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { render, unmountComponentAtNode } from "react-dom"; 4 | 5 | import * as Lib from "./core/lib"; 6 | import InfoTip from "./InfoTip"; 7 | 8 | class InfoTipHolder extends React.Component { 9 | listeners = []; 10 | maxWidth = 100; 11 | maxHeight = 80; 12 | infoTipMargin = 10; 13 | infoTipWindowPadding = 25; 14 | holder = React.createRef(); 15 | infoTip = React.createRef(); 16 | 17 | componentDidMount() { 18 | this.holder.current.addEventListener("mouseover", this.mousemove, false); 19 | this.holder.current.addEventListener("mouseover", this.mouseout, false); 20 | this.holder.current.addEventListener("mouseover", this.mousemove, false); 21 | } 22 | 23 | componentWillUnmount() { 24 | this.holder.current.removeEventListener("mouseover", this.mousemove, false); 25 | this.holder.current.removeEventListener("mouseover", this.mouseout, false); 26 | this.holder.current.removeEventListener("mouseover", this.mousemove, false); 27 | } 28 | 29 | setInfoTipState(state) { 30 | if (this.infoTip) { 31 | this.infoTip.current.setState(state); 32 | } 33 | } 34 | 35 | showInfoTip = (infoTip, target, x, y, rangeParent, rangeOffset) => { 36 | const scrollParent = Lib.getOverflowParent(target); 37 | const scrollX = x + (scrollParent ? scrollParent.scrollLeft : 0); 38 | 39 | // Distribute event to all registered listeners and show the info tip if 40 | // any of them return true. 41 | const dispatchArgs = [infoTip, target, scrollX, y, rangeParent, rangeOffset]; 42 | const result = Lib.dispatch2(this.listeners, "showInfoTip", dispatchArgs); 43 | 44 | if (result && result.element) { 45 | unmountComponentAtNode(infoTip); 46 | render(result.element, infoTip); 47 | 48 | this.setInfoTipState({ 49 | active: true, 50 | x, 51 | y, 52 | multiline: result.multiline, 53 | }); 54 | } else { 55 | this.setInfoTipState({ 56 | active: false, 57 | x, 58 | y, 59 | }); 60 | } 61 | } 62 | 63 | onInfoTipRef = (ref) => { 64 | this.infoTipDom = ref; 65 | } 66 | 67 | addListener(listener) { 68 | this.listeners.push(listener); 69 | } 70 | 71 | removeListener(listener) { 72 | Lib.remove(this.listeners, listener); 73 | } 74 | 75 | mousemove = (e) => { 76 | const x = e.clientX; 77 | const y = e.clientY; 78 | this.showInfoTip(this.infoTipDom, e.target, x, y, e.rangeParent, e.rangeOffset); 79 | } 80 | 81 | mouseout = (e) => { 82 | if (!e.relatedTarget) { 83 | this.setInfoTipState({ 84 | active: false, 85 | }); 86 | } 87 | } 88 | 89 | render() { 90 | const { infoTipMargin, infoTipWindowPadding } = this; 91 | return ( 92 |
93 | {this.props.children} 94 | 95 |
96 | ); 97 | } 98 | } 99 | 100 | InfoTipHolder.propTypes = { 101 | children: PropTypes.node, 102 | }; 103 | 104 | export default InfoTipHolder; 105 | -------------------------------------------------------------------------------- /webapp/modules/Stats.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import TimingPie from "./pie/TimingPie"; 4 | import ContentPie from "./pie/ContentPie"; 5 | import TrafficPie from "./pie/TrafficPie"; 6 | import CachePie from "./pie/CachePie"; 7 | 8 | class Stats extends Component { 9 | render() { 10 | return ( 11 |
12 | 13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default Stats; 22 | -------------------------------------------------------------------------------- /webapp/modules/booleanFlipper.js: -------------------------------------------------------------------------------- 1 | export default function booleanFlipper(idx) { 2 | return (value, i) => (idx === i) ? !value : value; 3 | } 4 | -------------------------------------------------------------------------------- /webapp/modules/buildInfo.js: -------------------------------------------------------------------------------- 1 | // __VERSION__ is supplied by webpack. 2 | // See webpage.config.js 3 | // eslint-disable-next-line 4 | export default __buildInfo__; 5 | -------------------------------------------------------------------------------- /webapp/modules/core/array.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/array 5 | */ 6 | define([ 7 | "./trace" 8 | ], 9 | 10 | function(Trace) { 11 | 12 | //***********************************************************************************************// 13 | 14 | /** 15 | * Helper functions for arrays. 16 | * @alias module:core/array 17 | */ 18 | var Arr = {}; 19 | 20 | //************************************************************************************************* 21 | // Arrays 22 | 23 | /** 24 | * @param {Object} object 25 | */ 26 | Arr.isArray = function(object) 27 | { 28 | // Supported by IE9 29 | // eslint-disable-next-line max-len 30 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray 31 | return Array.isArray(object); 32 | }; 33 | 34 | /** 35 | * @param {Array} array 36 | * @param {Function} fn 37 | */ 38 | Arr.cloneArray = function(array, fn) 39 | { 40 | var newArray = []; 41 | 42 | if (fn) 43 | for (var i = 0; i < array.length; ++i) 44 | newArray.push(fn(array[i])); 45 | else 46 | for (var j = 0; j < array.length; ++j) 47 | newArray.push(array[j]); 48 | 49 | return newArray; 50 | }; 51 | 52 | /** 53 | * @param {Array} array 54 | * @param {Number} index 55 | * @param {Array} other 56 | */ 57 | Arr.arrayInsert = function(array, index, other) 58 | { 59 | for (var i = 0; i < other.length; ++i) 60 | array.splice(i+index, 0, other[i]); 61 | return array; 62 | }; 63 | 64 | /** 65 | * @param {Array} list 66 | * @param {Object} item 67 | */ 68 | Arr.remove = function(list, item) 69 | { 70 | for (var i = 0; i < list.length; ++i) 71 | { 72 | if (list[i] === item) 73 | { 74 | list.splice(i, 1); 75 | return true; 76 | } 77 | } 78 | return false; 79 | }; 80 | 81 | // ********************************************************************************************* // 82 | 83 | return Arr; 84 | 85 | // ********************************************************************************************* // 86 | }); 87 | -------------------------------------------------------------------------------- /webapp/modules/core/cookies.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/cookies 5 | */ 6 | define([ 7 | "./string" 8 | ], 9 | 10 | function(Str) { 11 | 12 | //************************************************************************************************* 13 | 14 | /** 15 | * @return {string} Cookie path. 16 | */ 17 | function generateCookiePath() { 18 | var path = window.location.pathname; 19 | 20 | // Firefox 64 and IE 11 use trailing slash for client-side set cookies, 21 | // Chrome 71 does not. 22 | // So ensure cookie path ends in "/" for consistency 23 | // https://stackoverflow.com/a/53784228/319878 24 | if (!window.location.pathname.match(/\/$/)) { 25 | var parts = path.split("/"); 26 | parts.length -= 1; 27 | path = parts.join("/") + "/"; 28 | } 29 | 30 | return path; 31 | } 32 | 33 | /** 34 | * Helper functions for handling cookies. 35 | * @alias module:core/cookies 36 | */ 37 | var Cookies = 38 | { 39 | /** 40 | * @param {String} name The name of the cookie to get the value of. 41 | * @return {String} The cookie value if found, else `null` if the cookie exists but has no 42 | * value, else `undefined`. 43 | */ 44 | getCookie: function(name) 45 | { 46 | var cookies = document.cookie.split(";"); 47 | for (var i= 0; i 0 ? "-" : "+") + 101 | f(Math.abs(offsetHours)) + ":" + f(Math.abs(offsetMinutes)); 102 | 103 | return result + prettyOffset; 104 | }; 105 | 106 | // ********************************************************************************************* // 107 | 108 | return Date_; 109 | 110 | // ********************************************************************************************* // 111 | }); 112 | -------------------------------------------------------------------------------- /webapp/modules/core/json.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/json 5 | */ 6 | define([ 7 | "./trace" 8 | ], 9 | 10 | function(Trace) { 11 | 12 | //***********************************************************************************************// 13 | 14 | /** 15 | * @alias module:core/json 16 | */ 17 | var Json = {}; 18 | 19 | //***********************************************************************************************// 20 | // JSON 21 | 22 | /** 23 | * @param {Object} obj 24 | * @return {Object|null} 25 | */ 26 | Json.cloneJSON = function(obj) 27 | { 28 | if (obj === null || typeof(obj) !== "object") 29 | return obj; 30 | 31 | try 32 | { 33 | var temp = obj.constructor(); 34 | for (var key in obj) 35 | temp[key] = this.cloneJSON(obj[key]); 36 | return temp; 37 | } 38 | catch (err) 39 | { 40 | Trace.exception(err); 41 | } 42 | 43 | return null; 44 | }; 45 | 46 | // ********************************************************************************************* // 47 | 48 | return Json; 49 | 50 | // ********************************************************************************************* // 51 | }); 52 | -------------------------------------------------------------------------------- /webapp/modules/core/lib.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/lib 5 | */ 6 | define([ 7 | "./array", 8 | "./css", 9 | "./date", 10 | "./dom", 11 | "./events", 12 | "./json", 13 | "./mime", 14 | "./object", 15 | "./rect", 16 | "./sniff", 17 | "./string", 18 | "./url", 19 | "./trace" 20 | ], 21 | 22 | function(Arr, Css, Date_, Dom, Events, Json, Mime, Obj, Rect, Sniff, Str, Url, Trace) { 23 | 24 | //***********************************************************************************************// 25 | 26 | /** 27 | * @alias module:core/lib 28 | */ 29 | var Lib = {}; 30 | 31 | //***********************************************************************************************// 32 | // Browser Version 33 | 34 | Obj.append(Lib, Sniff); 35 | 36 | //***********************************************************************************************// 37 | // Core concepts (extension, bind) 38 | 39 | Obj.append(Lib, Obj); 40 | 41 | //***********************************************************************************************// 42 | // Events 43 | 44 | Obj.append(Lib, Events); 45 | 46 | //***********************************************************************************************// 47 | // Rect {top, left, height, width} 48 | 49 | Obj.append(Lib, Rect); 50 | 51 | //************************************************************************************************* 52 | // Arrays 53 | 54 | Obj.append(Lib, Arr); 55 | 56 | //************************************************************************************************* 57 | // Text Formatting 58 | 59 | Obj.append(Lib, Str); 60 | 61 | //************************************************************************************************* 62 | // Date 63 | 64 | Obj.append(Lib, Date_); 65 | 66 | //************************************************************************************************* 67 | // MIME 68 | 69 | Obj.append(Lib, Mime); 70 | 71 | //************************************************************************************************* 72 | // URL 73 | 74 | Obj.append(Lib, Url); 75 | 76 | //************************************************************************************************* 77 | // DOM 78 | 79 | Obj.append(Lib, Dom); 80 | 81 | //***********************************************************************************************// 82 | // CSS 83 | 84 | Obj.append(Lib, Css); 85 | 86 | //***********************************************************************************************// 87 | // JSON 88 | 89 | Obj.append(Lib, Json); 90 | 91 | // ********************************************************************************************* // 92 | 93 | return Lib; 94 | 95 | // ********************************************************************************************* // 96 | }); 97 | -------------------------------------------------------------------------------- /webapp/modules/core/mime.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/mime 5 | */ 6 | define([ 7 | "./trace" 8 | ], 9 | 10 | function(Trace) { 11 | 12 | //***********************************************************************************************// 13 | 14 | /** 15 | * @alias module:core/mime 16 | */ 17 | var Mime = {}; 18 | 19 | //************************************************************************************************* 20 | // MIME handling 21 | 22 | /** 23 | * @param {String} mimeType 24 | * @return {String} 25 | */ 26 | Mime.extractMimeType = function(mimeType) { 27 | if ("string" !== typeof mimeType) { 28 | throw new Error((typeof mimeType) + " is not of type string"); 29 | } 30 | // remove parameters (if any) 31 | var idx = mimeType.indexOf(";"); 32 | if (idx > -1) { 33 | mimeType = mimeType.substring(0, idx).trim(); 34 | } 35 | return mimeType; 36 | }; 37 | 38 | // ********************************************************************************************* // 39 | 40 | return Mime; 41 | 42 | // ********************************************************************************************* // 43 | }); 44 | -------------------------------------------------------------------------------- /webapp/modules/core/object.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/object 5 | */ 6 | define([ 7 | "./array", 8 | "./sniff" 9 | ], 10 | 11 | function(Arr, Sniff) { 12 | 13 | //***********************************************************************************************// 14 | 15 | /** 16 | * @alias module:core/object 17 | */ 18 | var Obj = {}; 19 | 20 | //***********************************************************************************************// 21 | // Type Checking 22 | 23 | var toString = Object.prototype.toString; 24 | var reFunction = /^\s*function(\s+[\w_$][\w\d_$]*)?\s*\(/; 25 | 26 | /** 27 | * @param {Object} object 28 | */ 29 | Obj.isFunction = function(object) 30 | { 31 | if (!object) 32 | return false; 33 | 34 | return toString.call(object) === "[object Function]" || 35 | Sniff.isIE && typeof object !== "string" && 36 | reFunction.test(String(object)); 37 | }; 38 | 39 | //***********************************************************************************************// 40 | // Core concepts (extension, bind) 41 | 42 | /** 43 | * @param {Object} l 44 | * @param {Object} r 45 | */ 46 | Obj.extend = function copyObject(l, r) 47 | { 48 | var m = {}; 49 | Obj.append(m, l); 50 | Obj.append(m, r); 51 | return m; 52 | }; 53 | 54 | /** 55 | * @param {Object} l 56 | * @param {Object} r 57 | */ 58 | Obj.append = function(l, r) 59 | { 60 | for (var n in r) 61 | l[n] = r[n]; 62 | return l; 63 | }; 64 | 65 | // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 66 | 67 | Obj.bind = function() // fn, thisObject, args => thisObject.fn(args, arguments); 68 | { 69 | var args = Arr.cloneArray(arguments); 70 | var fn = args.shift(); 71 | var object = args.shift(); 72 | return function bind() { 73 | var allArgs = args.concat(Arr.cloneArray(arguments)); 74 | return fn.apply(object, allArgs); 75 | }; 76 | }; 77 | 78 | Obj.bindFixed = function() // fn, thisObject, args => thisObject.fn(args); 79 | { 80 | var args = Arr.cloneArray(arguments); 81 | var fn = args.shift(); 82 | var object = args.shift(); 83 | return function bindFixed() { 84 | return fn.apply(object, args); 85 | }; 86 | }; 87 | 88 | // ********************************************************************************************* // 89 | 90 | return Obj; 91 | 92 | // ********************************************************************************************* // 93 | }); 94 | -------------------------------------------------------------------------------- /webapp/modules/core/rect.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * A module containing rectangle utilities. 5 | * @module core/rect 6 | */ 7 | define([ 8 | "./trace" 9 | ], 10 | 11 | function(Trace) { 12 | 13 | //***********************************************************************************************// 14 | 15 | /** 16 | * A rectangle type. 17 | * @typedef {Object} Rectangle 18 | * @property {Number} top 19 | * @property {Number} left 20 | * @property {Number} height 21 | * @property {Number} width 22 | */ 23 | 24 | /** 25 | * @alias module:core/rect 26 | */ 27 | var Rect = {}; 28 | 29 | //***********************************************************************************************// 30 | // Rect {top, left, height, width} 31 | 32 | /** 33 | * @param {module:core/rect~Rectangle} rect The rectangle to inflate. 34 | * @param {Number} x 35 | * @param {Number} y 36 | */ 37 | Rect.inflateRect = function(rect, x, y) 38 | { 39 | return { 40 | top: rect.top - y, 41 | left: rect.left - x, 42 | height: rect.height + 2*y, 43 | width: rect.width + 2*x 44 | }; 45 | }; 46 | 47 | /** 48 | * @param {module:core/rect~Rectangle} rect The rectangle to test. 49 | * @param {Number} x 50 | * @param {Number} y 51 | */ 52 | Rect.pointInRect = function(rect, x, y) 53 | { 54 | return (y >= rect.top && y <= rect.top + rect.height && 55 | x >= rect.left && x <= rect.left + rect.width); 56 | }; 57 | 58 | // ********************************************************************************************* // 59 | 60 | return Rect; 61 | 62 | // ********************************************************************************************* // 63 | }); 64 | -------------------------------------------------------------------------------- /webapp/modules/core/sniff.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/sniff 5 | */ 6 | define([ 7 | "./trace" 8 | ], 9 | 10 | function(Trace) { 11 | 12 | //***********************************************************************************************// 13 | 14 | var Sniff = {}; 15 | 16 | //***********************************************************************************************// 17 | // Browser Version 18 | 19 | var hasNav = ("undefined" !== typeof navigator); 20 | var hasWin = ("undefined" !== typeof window); 21 | var userAgent = hasNav ? navigator.userAgent.toLowerCase() : ""; 22 | 23 | Sniff.isFirefox = /firefox/.test(userAgent); 24 | Sniff.isOpera = /opera/.test(userAgent); 25 | Sniff.isWebkit = /webkit/.test(userAgent); 26 | Sniff.isSafari = /webkit/.test(userAgent); 27 | Sniff.isIE = /msie/.test(userAgent) && !/opera/.test(userAgent); 28 | Sniff.isIE6 = hasNav && /msie 6/i.test(navigator.appVersion); 29 | Sniff.browserVersion = (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1]; 30 | Sniff.isIElt8 = Sniff.isIE && (Sniff.browserVersion-0 < 8); 31 | Sniff.supportsSelectElementText = hasWin && ((window.getSelection && window.document.createRange) || 32 | (window.document.body.createTextRange)); 33 | 34 | // ********************************************************************************************* // 35 | 36 | return Sniff; 37 | 38 | // ********************************************************************************************* // 39 | }); 40 | -------------------------------------------------------------------------------- /webapp/modules/core/trace.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * @module core/trace 5 | */ 6 | define([ 7 | ], 8 | 9 | function() { 10 | 11 | //************************************************************************************************* 12 | 13 | var Trace = { 14 | log: function(){}, 15 | error: function(){}, 16 | exception: function(){}, 17 | time: function(){}, 18 | timeEnd: function(){} 19 | }; 20 | 21 | if (typeof(console) === "undefined") 22 | return Trace; 23 | 24 | //@ifdef DEBUG 25 | Trace.log = function() 26 | { 27 | if (typeof(console.log) === "function") 28 | console.log.apply(console, arguments); 29 | }; 30 | 31 | Trace.time = function() 32 | { 33 | if (typeof(console.time) === "function") 34 | console.time.apply(console, arguments); 35 | }; 36 | 37 | Trace.timeEnd = function(name, message) 38 | { 39 | if (typeof(console.timeEnd) === "function") 40 | console.timeEnd.apply(console, arguments); 41 | }; 42 | //@endif 43 | 44 | Trace.error = function() 45 | { 46 | if (typeof(console.error) === "function") 47 | console.error.apply(console, arguments); 48 | }; 49 | 50 | Trace.exception = function() 51 | { 52 | if (typeof(console.error) === "function") 53 | console.error.apply(console, arguments); 54 | }; 55 | 56 | return Trace; 57 | 58 | //************************************************************************************************* 59 | }); 60 | -------------------------------------------------------------------------------- /webapp/modules/deferred.js: -------------------------------------------------------------------------------- 1 | export default function deferred() { 2 | let resolve; 3 | let reject; 4 | const promise = new Promise((...args) => { 5 | [resolve, reject] = args; 6 | }); 7 | return { 8 | resolve, 9 | reject, 10 | promise, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /webapp/modules/intersperse.js: -------------------------------------------------------------------------------- 1 | export default function intersperse(arr, supplier) { 2 | const len = arr.length; 3 | return arr.reduce((arr2, existingItem, i) => { 4 | const isLastItem = (i === len - 1); 5 | const newItem = (typeof supplier === "function") 6 | ? supplier(existingItem, i) 7 | : supplier; 8 | arr2.push(existingItem); 9 | if (!isLastItem) { 10 | arr2.push(newItem); 11 | } 12 | return arr2; 13 | }, []); 14 | } 15 | -------------------------------------------------------------------------------- /webapp/modules/nettable/Bar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class Bar extends Component { 5 | render() { 6 | const { bar } = this.props; 7 | 8 | const timeLabel = bar.timeLabel ? {bar.timeLabel} : null; 9 | 10 | return ( 11 |
12 | {timeLabel} 13 |
14 | ); 15 | } 16 | } 17 | 18 | Bar.propTypes = { 19 | bar: PropTypes.object, 20 | }; 21 | 22 | export default Bar; 23 | -------------------------------------------------------------------------------- /webapp/modules/nettable/NetInfoRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Strings from "i18n!../nls/requestBody"; 5 | 6 | import TabView from "../tabview/TabView"; 7 | import Headers from "../requestbodies/Headers"; 8 | import PlainResponse from "../requestbodies/PlainResponse"; 9 | import Highlighted from "../requestbodies/Highlighted"; 10 | import URLParameters from "../requestbodies/URLParameters"; 11 | import SendData from "../requestbodies/SendData"; 12 | import DataURL from "../requestbodies/DataURL"; 13 | import JSONEntryTree from "../requestbodies/JSONEntryTree"; 14 | import XMLEntryTree from "../requestbodies/XMLEntryTree"; 15 | import ExternalImage from "../requestbodies/ExternalImage"; 16 | import Image from "../requestbodies/Image"; 17 | 18 | const responseBodyComponents = { 19 | Headers: { 20 | Component: Headers, 21 | id: "Headers", 22 | label: Strings.Headers, 23 | }, 24 | PlainResponse: { 25 | Component: PlainResponse, 26 | id: "Response", 27 | label: Strings.Response, 28 | }, 29 | Highlighted: { 30 | Component: Highlighted, 31 | id: "Highlighted", 32 | label: Strings.Highlighted, 33 | }, 34 | URLParameters: { 35 | Component: URLParameters, 36 | id: "Params", 37 | label: Strings.URLParameters, 38 | }, 39 | SendData: { 40 | Component: SendData, 41 | id: "Post", 42 | // TODO, this has to be determined on-the-fly by entry.request.method 43 | label: "Post", 44 | }, 45 | DataURL: { 46 | Component: DataURL, 47 | id: "DataURL", 48 | label: Strings.DataURL, 49 | }, 50 | JSON: { 51 | Component: JSONEntryTree, 52 | id: "JSON", 53 | label: Strings.JSON, 54 | }, 55 | XML: { 56 | Component: XMLEntryTree, 57 | id: "XML", 58 | label: Strings.XML, 59 | }, 60 | Image: { 61 | Component: Image, 62 | id: "Image", 63 | label: Strings.Image, 64 | }, 65 | ExternalImage: { 66 | Component: ExternalImage, 67 | id: "ExternalImage", 68 | label: Strings.ExternalImage, 69 | }, 70 | }; 71 | 72 | function createTabs(entry) { 73 | const tabs = []; 74 | Object.keys(responseBodyComponents).forEach((name) => { 75 | const Component = responseBodyComponents[name].Component; 76 | if (!Component.canShowEntry || Component.canShowEntry(entry)) { 77 | const info = responseBodyComponents[name]; 78 | tabs.push(Object.assign({}, info, { 79 | body: , 80 | })); 81 | } 82 | }); 83 | return tabs; 84 | } 85 | 86 | class NetInfoRow extends Component { 87 | state = { 88 | selectedTabIdx: 0, 89 | } 90 | 91 | setSelectedTab = (selectedTabIdx) => { 92 | if (typeof selectedTabIdx === "string") { 93 | selectedTabIdx = this.tabs.findIndex(({ id }) => id === selectedTabIdx); 94 | } 95 | this.setState({ selectedTabIdx }); 96 | } 97 | 98 | render() { 99 | const { entry } = this.props; 100 | this.tabs = createTabs(entry); 101 | return ( 102 | 103 | 104 | 110 | 111 | 112 | ); 113 | } 114 | } 115 | 116 | NetInfoRow.propTypes = { 117 | entry: PropTypes.object, 118 | }; 119 | 120 | export default NetInfoRow; 121 | -------------------------------------------------------------------------------- /webapp/modules/nettable/NetRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Url from "../core/url"; 5 | import * as Str from "../core/string"; 6 | import Bar from "./Bar"; 7 | import PageTimingBar from "./PageTimingBar"; 8 | 9 | class NetRow extends Component { 10 | getHref(entry) { 11 | const fileName = Url.getFileName(this.getFullHref(entry)); 12 | return unescape(entry.request.method + " " + fileName); 13 | } 14 | 15 | getFullHref(entry) { 16 | return unescape(entry.request.url); 17 | } 18 | 19 | getStatus(entry) { 20 | const status = entry.response.status > 0 ? (entry.response.status + " ") : ""; 21 | return status + entry.response.statusText; 22 | } 23 | 24 | getType(entry) { 25 | return entry.response.content.mimeType; 26 | } 27 | 28 | getDomain(entry) { 29 | return Url.getPrettyDomain(entry.request.url); 30 | } 31 | 32 | getSize(entry) { 33 | const bodySize = entry.response.bodySize; 34 | const size = (bodySize && bodySize !== -1) ? bodySize : entry.response.content.size; 35 | 36 | return this.formatSize(size); 37 | } 38 | 39 | formatSize(bytes) { 40 | return Str.formatSize(bytes); 41 | } 42 | 43 | shouldComponentUpdate(nextProps, nextState) { 44 | const { entry, entryId, opened, bars, pageTimingBars } = this.props; 45 | // TODO - Very basic, needs improving 46 | return !( 47 | entry === nextProps.entry && 48 | entryId === nextProps.entryId && 49 | opened === nextProps.opened && 50 | bars.length === nextProps.bars.length && 51 | pageTimingBars.length === nextProps.pageTimingBars.length 52 | ); 53 | } 54 | 55 | render() { 56 | const { entry, entryId, breakLayout, opened, bars, pageTimingBars, onClick } = this.props; 57 | 58 | const barComponents = (bars || []).map( 59 | (bar, i) => 60 | ); 61 | 62 | const pageTimingBarComponents = (pageTimingBars || []).map( 63 | (pageTimingBar, i) => 64 | ); 65 | 66 | return ( 67 | 68 | 69 |
70 | {this.getHref(entry)} 71 |
72 |
73 | {this.getFullHref(entry)} 74 |
75 | 76 | 77 |
78 | {this.getStatus(entry)} 79 |
80 | 81 | 82 |
83 | {this.getType(entry)} 84 |
85 | 86 | 87 |
88 | {this.getDomain(entry)} 89 |
90 | 91 | 92 |
93 | {this.getSize(entry)} 94 |
95 | 96 | 97 |
  98 | {barComponents} 99 | {pageTimingBarComponents} 100 |
101 | 102 | 103 |
104 | 105 | 106 | ); 107 | } 108 | } 109 | 110 | NetRow.propTypes = { 111 | entry: PropTypes.object, 112 | entryId: PropTypes.number, 113 | opened: PropTypes.bool, 114 | bars: PropTypes.array, 115 | pageTimingBars: PropTypes.array, 116 | onClick: PropTypes.func, 117 | }; 118 | 119 | export default NetRow; 120 | -------------------------------------------------------------------------------- /webapp/modules/nettable/NetSummaryRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Strings from "i18n!../nls/requestList"; 5 | import * as Str from "../core/string"; 6 | import { calculateSummaryInfo } from "./NetSummaryRowModel"; 7 | 8 | class NetSummaryRow extends React.Component { 9 | formatRequestCount(count) { 10 | return count + " " + (count === 1 ? Strings.request : Strings.requests); 11 | } 12 | 13 | formatTotalTime(totalTime, onLoadTime) { 14 | const onLoadStr = onLoadTime > 0 ? " (onload: " + Str.formatTime(onLoadTime) + ")" : ""; 15 | return Str.formatTime(totalTime) + onLoadStr; 16 | } 17 | 18 | getCacheSizeContent(cachedSize) { 19 | if (cachedSize <= 0) { 20 | return ""; 21 | } 22 | return [ 23 | "(", 24 | {Str.formatSize(cachedSize)}, 25 | {Strings.summaryFromCache}, 26 | ")", 27 | ]; 28 | } 29 | 30 | render() { 31 | let { page, entries, summaryInfo } = this.props; 32 | if (!summaryInfo) { 33 | summaryInfo = calculateSummaryInfo(page, entries); 34 | } 35 | 36 | return ( 37 | 38 | 39 | 40 |
{this.formatRequestCount(entries.length)}
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
{Str.formatSize(summaryInfo.totalTransferredSize)}
49 | 50 | 51 |
52 |
53 | {this.getCacheSizeContent(summaryInfo.cachedSize)} 54 |
55 |
56 | ({Str.formatSize(summaryInfo.totalUncompressedSize)} 57 | {Strings.uncompressed}) 58 |
59 |
60 | {this.formatTotalTime(summaryInfo.totalTime, summaryInfo.onLoadTime)} 61 |
62 |
63 | 64 | 65 | 66 | ); 67 | } 68 | }; 69 | 70 | NetSummaryRow.propTypes = { 71 | entries: PropTypes.array.isRequired, 72 | page: PropTypes.object, 73 | summaryInfo: PropTypes.object, 74 | }; 75 | 76 | export default NetSummaryRow; 77 | -------------------------------------------------------------------------------- /webapp/modules/nettable/NetSummaryRowModel.js: -------------------------------------------------------------------------------- 1 | import HarModel from "../preview/harModel"; 2 | import * as Date_ from "../core/date"; 3 | 4 | export function summarizeEntries(entries) { 5 | let cachedSize = 0; 6 | let totalTransferredSize = 0; 7 | let totalUncompressedSize = 0; 8 | 9 | let fileCount = 0; 10 | let minTime = 0; 11 | let maxTime = 0; 12 | 13 | for (let i = 0; i < entries.length; i++) { 14 | let file = entries[i]; 15 | let startedDateTime = Date_.parseISO8601(file.startedDateTime); 16 | 17 | ++fileCount; 18 | 19 | let transferredSize = HarModel.getEntryTransferredSize(file); 20 | let uncompressedSize = HarModel.getEntryUncompressedSize(file); 21 | 22 | totalTransferredSize += transferredSize; 23 | totalUncompressedSize += uncompressedSize; 24 | 25 | if (HarModel.isCachedEntry(file)) { 26 | cachedSize += uncompressedSize; 27 | } 28 | 29 | if (!minTime || startedDateTime < minTime) { 30 | minTime = startedDateTime; 31 | } 32 | 33 | let fileEndTime = startedDateTime + file.time; 34 | if (fileEndTime > maxTime) { 35 | maxTime = fileEndTime; 36 | } 37 | } 38 | 39 | let totalTime = maxTime - minTime; 40 | return { 41 | cachedSize, 42 | totalUncompressedSize, 43 | totalTransferredSize, 44 | totalTime, 45 | fileCount 46 | }; 47 | } 48 | 49 | export function calculateSummaryInfo(page, entries) { 50 | let summaryInfo = summarizeEntries(entries); 51 | summaryInfo.onLoadTime = 0; 52 | if (page) { 53 | summaryInfo.onLoadTime = page.pageTimings.onLoad; 54 | } 55 | return summaryInfo; 56 | } 57 | -------------------------------------------------------------------------------- /webapp/modules/nettable/PageTimingBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class PageTimingBar extends Component { 5 | render() { 6 | const { left, classes } = this.props; 7 | const style = { 8 | left: left, 9 | display: "block", 10 | }; 11 | return ( 12 |
13 | ); 14 | } 15 | } 16 | 17 | PageTimingBar.propTypes = { 18 | classes: PropTypes.string, 19 | left: PropTypes.string, 20 | }; 21 | 22 | export default PageTimingBar; 23 | -------------------------------------------------------------------------------- /webapp/modules/nettable/defaultTimingDefinitions.js: -------------------------------------------------------------------------------- 1 | import Strings from "i18n!../nls/requestList"; 2 | 3 | export default [ 4 | { 5 | name: "onContentLoad", 6 | classes: "netContentLoadBar", 7 | description: Strings.ContentLoad, 8 | }, 9 | { 10 | name: "onLoad", 11 | classes: "netWindowLoadBar", 12 | description: Strings.WindowLoad, 13 | }, 14 | ]; 15 | -------------------------------------------------------------------------------- /webapp/modules/nettable/testData.js: -------------------------------------------------------------------------------- 1 | const entry = { 2 | request: { 3 | method: "GET", 4 | url: "http://www.softwareishard.com/blog/firebug/firebug-16-beta-1-released/" 5 | }, 6 | response: { 7 | status: 200, 8 | statusText: "OK", 9 | bodySize: 31918, 10 | content: { 11 | mimeType: "text/html" 12 | } 13 | } 14 | }; 15 | 16 | const bars = [ 17 | { 18 | className: "netBlockingBar", 19 | style: { 20 | left: "0%", 21 | width: "0%" 22 | } 23 | }, 24 | { 25 | className: "netResolvingBar", 26 | style: { 27 | left: "0%", 28 | width: "0%" 29 | } 30 | }, 31 | { 32 | className: "netConnectingBar", 33 | style: { 34 | left: "0%", 35 | width: "0.852%" 36 | } 37 | }, 38 | { 39 | className: "netSendingBar", 40 | style: { 41 | left: "0%", 42 | width: "0.852%" 43 | } 44 | }, 45 | { 46 | className: "netWaitingBar", 47 | style: { 48 | left: "0%", 49 | width: "73.442%" 50 | } 51 | }, 52 | { 53 | className: "netReceivingBar", 54 | style: { 55 | left: "0%", 56 | width: "77.352%" 57 | }, 58 | timeLabel: "2s" 59 | } 60 | ]; 61 | 62 | export { entry, bars }; 63 | -------------------------------------------------------------------------------- /webapp/modules/nls/domTab.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "domTabLabel": "HAR", 7 | "searchDisabledForIE": "You need Mozilla or WebKit based browser to search in HAR", 8 | "searchOptionJsonQuery": "JSON Query", 9 | "tableView": "Table View", 10 | "searchResultsDefaultText": "JSON Query Results", 11 | "searchPlaceholder": "Search", 12 | "jsonQueryPlaceholder": "JSON Query", 13 | "queryResultsTableView": "Table View" 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /webapp/modules/nls/harModel.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "validationType": "HAR Validation", 7 | "validationSumTimeError": "Sum of request timings doesn't correspond to the total value: " + 8 | "%S (request.time: %S vs. sum: %S), request#: %S, parent page: %S", 9 | "validationNegativeTimeError": "Negative time is not allowed: " + 10 | "%S, request#: %S, parent page: %S" 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /webapp/modules/nls/harStats.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "pieLabelDNS": "DNS", 7 | "pieLabelSSL": "SSL/TLS", 8 | "pieLabelConnect": "Connect", 9 | "pieLabelBlocked": "Blocked", 10 | "pieLabelSend": "Send", 11 | "pieLabelWait": "Wait", 12 | "pieLabelReceive": "Receive", 13 | 14 | "pieLabelHTMLText": "HTML/Text", 15 | "pieLabelJavaScript": "JavaScript", 16 | "pieLabelCSS": "CSS", 17 | "pieLabelImage": "Image", 18 | "pieLabelFlash": "Flash", 19 | "pieLabelOthers": "Others", 20 | 21 | "pieLabelHeadersSent": "Headers Sent", 22 | "pieLabelBodiesSent": "Bodies Sent", 23 | "pieLabelHeadersReceived": "Headers Received", 24 | "pieLabelBodiesReceived": "Bodies Received", 25 | 26 | "pieLabelDownloaded": "Downloaded", 27 | "pieLabelPartial": "Partial", 28 | "pieLabelFromCache": "From Cache" 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /webapp/modules/nls/harViewer.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "aboutTabLabel": "About", 7 | "schemaTabLabel": "Schema" 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /webapp/modules/nls/homeTab.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "homeTabLabel": "Home", 7 | "loadingHar": "Loading..." 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /webapp/modules/nls/pageList.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "column.label.index": "Index", 7 | "column.label.url": "URL", 8 | "column.label.status": "Status", 9 | "column.label.type": "Type", 10 | "column.label.domain": "Domain", 11 | "column.label.serverIPAddress": "Server IP Address", 12 | "column.label.connection": "Connection", 13 | "column.label.size": "Compressed Size", 14 | "column.label.uncompressedSize": "Uncompressed Size", 15 | "column.label.timeline": "Timeline", 16 | "action.label.Reset": "Reset" 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /webapp/modules/nls/pageTimeline.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "pageLoad": "Page Load", 7 | "request": "Request", 8 | "requests": "Requests", 9 | "pageBarTooltip": "Click to select and include in statistics preview." 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /webapp/modules/nls/previewTab.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "previewTabLabel": "Preview", 7 | "showTimelineButton": "Show Page Timeline", 8 | "hideTimelineButton": "Hide Page Timeline", 9 | "showTimelineTooltip": "Show/hide statistic preview for selected pages in the timeline.", 10 | "showStatsButton": "Show Statistics", 11 | "hideStatsButton": "Hide Statistics", 12 | "showStatsTooltip": "Show/hide page timeline.", 13 | "clearButton": "Clear", 14 | "clearTooltip": "Remove all HAR logs from the viewer", 15 | "downloadTooltip": "Download all current data in one HAR file.", 16 | "downloadError": "Failed to save HAR data", 17 | "menuShowHARSource": "Show HAR Source" 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /webapp/modules/nls/requestBody.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "RequestVersion": "Request Version", 7 | "ResponseVersion": "Response Version", 8 | "RequestHeaders": "Request Headers", 9 | "ResponseHeaders": "Response Headers", 10 | "RequestCookies": "Request Cookies", 11 | "ResponseCookies": "Response Cookies", 12 | "URLParameters": "Params", 13 | "Headers": "Headers", 14 | "Post": "Post", 15 | "Put": "Put", 16 | "Get": "Get", 17 | "Cookies": "Cookies", 18 | "Response": "Response", 19 | "Highlighted": "Highlighted", 20 | "Image": "Img (from HAR)", 21 | "ImageTitle": "Uses image payload data from the HAR", 22 | "ExternalImage": "Img (from URL)", 23 | "ExternalImageTitle": "Creates an IMG tag whose src is the request URL", 24 | "Cache": "Cache", 25 | "HTML": "HTML", 26 | "JSON": "JSON", 27 | "XML": "XML", 28 | "DataURL": "Data URL" 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /webapp/modules/nls/requestList.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "summaryFromCache": "From Cache", 7 | "resourceFromCache": "Resource from cache", 8 | "uncompressed": "Uncompressed", 9 | "menuBreakLayout": "Break Timeline Layout", 10 | "menuOpenRequestInWindow": "Open Request in New Window", 11 | "menuOpenResponseInWindow": "Open Response in New Window", 12 | "request": "Request", 13 | "requests": "Requests", 14 | 15 | "tooltipSize": "%S (%S bytes)", 16 | "tooltipZippedSize": "%S (%S bytes) - compressed", 17 | "tooltipUnzippedSize": "%S (%S bytes) - uncompressed", 18 | "unknownSize": "Unknown size", 19 | 20 | "request.Started": "Request start time since the beginning", 21 | "request.phases.label": "Request phases start and elapsed time relative to the request start:", 22 | "request.phase.Resolving": "DNS Lookup", 23 | "request.phase.Connecting": "Connecting", 24 | "request.phase.Blocking": "Blocking", 25 | "request.phase.Sending": "Sending", 26 | "request.phase.Waiting": "Waiting", 27 | "request.phase.Receiving": "Receiving", 28 | 29 | "request.timings.label": "Event timing relative to the request start:", 30 | "ContentLoad": "DOM Loaded", 31 | "WindowLoad": "Page Loaded", 32 | "page.event.Load": "Page Loaded", 33 | 34 | "menuBreakTimeline": "Break Timeline Layout", 35 | "menuOpenRequest": "Open Request in New Window", 36 | "menuOpenResponse": "Open Response in New Window" 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /webapp/modules/nls/search.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "search": "Search", 7 | "caseSensitive": "Case Sensitive" 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /webapp/modules/nls/tableView.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define( 4 | { 5 | "root": { 6 | "objectProperties": "Object Properties" 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /webapp/modules/pagetable/PageInfoRow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import NetTable from "../nettable/NetTable"; 5 | 6 | class PageInfoRow extends Component { 7 | shouldComponentUpdate(nextProps, nextState) { 8 | const { model, page } = this.props; 9 | return !(model === nextProps.model && page === nextProps.page); 10 | } 11 | 12 | render() { 13 | const { model, page } = this.props; 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | PageInfoRow.propTypes = { 25 | model: PropTypes.object, 26 | page: PropTypes.object, 27 | }; 28 | 29 | export default PageInfoRow; 30 | -------------------------------------------------------------------------------- /webapp/modules/pagetable/PageRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import * as Str from "../core/string"; 4 | 5 | function getPageTitle(page) { 6 | return Str.cropString(page.title, 100); 7 | } 8 | 9 | const PageRow = (props) => { 10 | return ( 11 | 12 | {getPageTitle(props.page)} 13 |
14 | 15 | ); 16 | }; 17 | 18 | PageRow.propTypes = { 19 | onClick: PropTypes.func, 20 | opened: PropTypes.bool, 21 | page: PropTypes.object, 22 | }; 23 | 24 | export default PageRow; 25 | -------------------------------------------------------------------------------- /webapp/modules/pagetable/PageTable.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import booleanFlipper from "../booleanFlipper"; 5 | import PageRow from "./PageRow"; 6 | import PageInfoRow from "./PageInfoRow"; 7 | 8 | class PageTable extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | const { model } = this.props; 13 | 14 | this.state = {}; 15 | 16 | if (model && model.input) { 17 | this.state.pageRowExpandedState = this.getInitialExpandedState(); 18 | } 19 | } 20 | 21 | getInitialExpandedState() { 22 | const { model, expandAll } = this.props; 23 | const pages = model.input.log.pages || []; 24 | const pageRowExpandedState = pages.map(() => expandAll); 25 | if (pageRowExpandedState.length === 1) { 26 | pageRowExpandedState[0] = true; 27 | } 28 | return pageRowExpandedState; 29 | } 30 | 31 | createPageRows(model) { 32 | const rows = []; 33 | 34 | if (!model || !model.input) { 35 | return rows; 36 | } 37 | 38 | const { pages } = model.input.log; 39 | 40 | if (pages) { 41 | const { pageRowExpandedState } = this.state; 42 | 43 | // Use concat to flatten an array of arrays to a flat array. 44 | return rows.concat(pages.map((page, i) => { 45 | const opened = pageRowExpandedState[i]; 46 | const pageRow = ; 47 | if (!opened) { 48 | return pageRow; 49 | } 50 | return [pageRow, ]; 51 | })); 52 | } 53 | 54 | return rows; 55 | } 56 | 57 | onPageRowClick(pageRowIdx) { 58 | this.setState(({ pageRowExpandedState }) => ({ 59 | pageRowExpandedState: pageRowExpandedState.map(booleanFlipper(pageRowIdx)), 60 | })); 61 | } 62 | 63 | render() { 64 | const { model } = this.props; 65 | const pageRows = this.createPageRows(model); 66 | 67 | return ( 68 | 69 | 70 | {pageRows} 71 | 72 |
73 | ); 74 | } 75 | }; 76 | 77 | PageTable.displayName = "pagetable/PageTable"; 78 | 79 | PageTable.propTypes = { 80 | model: PropTypes.object, 81 | expandAll: PropTypes.bool, 82 | }; 83 | 84 | export default PageTable; 85 | -------------------------------------------------------------------------------- /webapp/modules/pagetimeline/PageDescContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Date_ from "../core/date"; 5 | import * as Str from "../core/string"; 6 | import Strings from "i18n!../nls/PageTimeline"; 7 | 8 | class PageDescContainer extends Component { 9 | render() { 10 | const { page, numEntries } = this.props; 11 | return ( 12 |
13 |
14 |
15 | { 16 | page ? this.getSummary(page, numEntries) : "" 17 | } 18 | { 19 | page ? this.getTime(this.props.page) : "" 20 | } 21 | { 22 | page ? this.getTitle(this.props.page) : "" 23 | } 24 |
{
25 |             page ? this.getComment(this.props.page) : ""
26 |           }
27 |
28 |
29 | ); 30 | } 31 | 32 | getSummary(page, numEntries) { 33 | let summary = ""; 34 | const onLoad = page.pageTimings.onLoad; 35 | if (onLoad > 0) { 36 | summary += Strings.pageLoad + ": " + Str.formatTime(onLoad.toFixed(2)) + ", "; 37 | } 38 | 39 | const count = numEntries; 40 | summary += count + " " + (count === 1 ? Strings.request : Strings.requests); 41 | 42 | return summary; 43 | } 44 | 45 | getTime(page) { 46 | const pageStart = Date_.parseISO8601(page.startedDateTime); 47 | const date = new Date(pageStart); 48 | return date.toLocaleString(); 49 | } 50 | 51 | getTitle(page) { 52 | return page.title; 53 | } 54 | 55 | getComment(page) { 56 | return page.comment ? page.comment : ""; 57 | } 58 | } 59 | 60 | PageDescContainer.propTypes = { 61 | numEntries: PropTypes.number, 62 | page: PropTypes.object, 63 | }; 64 | 65 | export default PageDescContainer; 66 | -------------------------------------------------------------------------------- /webapp/modules/pagetimeline/PageTimelineCol.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class PageTimelineCol extends Component { 5 | tdRef = React.createRef() 6 | 7 | render() { 8 | const { onClick, onMouseOver, selected } = this.props; 9 | const title = "Click to select and include in statistics preview."; 10 | const style = { 11 | height: this.getHeight() + "px", 12 | }; 13 | return ( 14 | 15 |
20 | 21 | ); 22 | } 23 | 24 | getHeight() { 25 | const { page, maxElapsedTime } = this.props; 26 | const { onLoad } = page.pageTimings; 27 | 28 | let height = 1; 29 | if (onLoad > 0 && maxElapsedTime > 0) { 30 | height = Math.round((onLoad / maxElapsedTime) * 100); 31 | } 32 | 33 | return Math.max(1, height); 34 | } 35 | } 36 | 37 | PageTimelineCol.propTypes = { 38 | maxElapsedTime: PropTypes.number, 39 | onClick: PropTypes.func, 40 | onMouseOver: PropTypes.func, 41 | page: PropTypes.object, 42 | selected: PropTypes.bool, 43 | }; 44 | 45 | export default PageTimelineCol; 46 | -------------------------------------------------------------------------------- /webapp/modules/pagetimeline/PageTimelineTable.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class PageTimelineTable extends Component { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | {this.props.pageTimelineCols} 11 | 12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | PageTimelineTable.propTypes = { 19 | pageTimelineCols: PropTypes.node, 20 | }; 21 | 22 | export default PageTimelineTable; 23 | -------------------------------------------------------------------------------- /webapp/modules/pagetimeline/Selection.js: -------------------------------------------------------------------------------- 1 | import * as Css from "../core/css"; 2 | import * as Dom from "../core/dom"; 3 | 4 | const Selection = { 5 | isSelected: function(bar) { 6 | return Css.hasClass(bar, "selected"); 7 | }, 8 | 9 | toggle: function(bar) { 10 | Css.toggleClass(bar, "selected"); 11 | }, 12 | 13 | select: function(bar) { 14 | if (!this.isSelected(bar)) { 15 | Css.setClass(bar, "selected"); 16 | } 17 | }, 18 | 19 | unselect: function(bar) { 20 | if (this.isSelected(bar)) { 21 | Css.removeClass(bar, "selected"); 22 | } 23 | }, 24 | 25 | getSelection: function(row) { 26 | const bars = Array.from(Dom.getElementsByClass(row, "pageBar")); 27 | return bars 28 | .filter((bar) => this.isSelected(bar)) 29 | .map((bar) => bar.page); 30 | }, 31 | 32 | unselectAll: function(row, except) { 33 | const bars = Array.from(Dom.getElementsByClass(row, "pageBar")); 34 | bars 35 | .filter((bar) => except !== bar) 36 | .forEach((bar) => this.unselect(bar)); 37 | }, 38 | }; 39 | 40 | export default Selection; 41 | -------------------------------------------------------------------------------- /webapp/modules/pie/CachePie.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Pie from "./Pie"; 5 | import * as Str from "../core/string"; 6 | import Strings from "i18n!../nls/harStats"; 7 | 8 | class CachePie extends Component { 9 | title = "Comparison of downloaded data from the server and browser cache." 10 | 11 | data = [ 12 | { 13 | count: 0, 14 | value: 0, 15 | label: Strings.pieLabelDownloaded, 16 | color: "rgb(182, 182, 182)", 17 | }, 18 | { 19 | count: 0, 20 | value: 0, 21 | label: Strings.pieLabelPartial, 22 | color: "rgb(218, 218, 218)", 23 | }, 24 | { 25 | count: 0, 26 | value: 0, 27 | label: Strings.pieLabelFromCache, 28 | color: "rgb(239, 239, 239)", 29 | }, 30 | ] 31 | 32 | getLabelTooltipText = (item) => { 33 | return item.count + "x " + item.label + ": " + Str.formatSize(item.value); 34 | } 35 | 36 | calcData() { 37 | // Iterate over all requests and compute stats. 38 | const { entries = [] } = this.props; 39 | const data = this.data.map((d) => ({ ...d })); 40 | entries.forEach((entry) => { 41 | if (!entry.timings) { 42 | return; 43 | } 44 | 45 | const { response } = entry; 46 | // TODO use transferred size 47 | const resBodySize = response.bodySize > 0 ? response.bodySize : 0; 48 | 49 | // TODO use proper stats routines 50 | 51 | // Get Cache info 52 | if (entry.response.status === 206) { // Partial content 53 | data[1].value += resBodySize; 54 | data[1].count++; 55 | } else if (entry.response.status === 304) { // From cache 56 | data[2].value += resBodySize; 57 | data[2].count++; 58 | } else if (resBodySize > 0) { // Downloaded 59 | data[0].value += resBodySize; 60 | data[0].count++; 61 | } 62 | }); 63 | return data; 64 | } 65 | 66 | render() { 67 | return ; 68 | } 69 | } 70 | 71 | CachePie.propTypes = { 72 | entries: PropTypes.array, 73 | }; 74 | 75 | export default CachePie; 76 | -------------------------------------------------------------------------------- /webapp/modules/pie/ContentPie.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Pie from "./Pie"; 5 | import * as Mime from "../core/mime"; 6 | import * as Str from "../core/string"; 7 | import Strings from "i18n!../nls/harStats"; 8 | 9 | const jsTypes = { 10 | "text/javascript": 1, 11 | "text/jscript": 1, 12 | "application/javascript": 1, 13 | "application/x-javascript": 1, 14 | "text/js": 1, 15 | }; 16 | 17 | const htmlTypes = { 18 | "text/plain": 1, 19 | "text/html": 1, 20 | }; 21 | 22 | const cssTypes = { 23 | "text/css": 1, 24 | }; 25 | 26 | const imageTypes = { 27 | "image/png": 1, 28 | "image/jpeg": 1, 29 | "image/gif": 1, 30 | }; 31 | 32 | const flashTypes = { 33 | "application/x-shockwave-flash": 1, 34 | }; 35 | 36 | const jsonTypes = { 37 | "text/x-json": 1, 38 | "text/x-js": 1, 39 | "application/json": 1, 40 | "application/x-js": 1, 41 | }; 42 | 43 | const xmlTypes = { 44 | "application/xml": 1, 45 | "application/xhtml+xml": 1, 46 | "application/vnd.mozilla.xul+xml": 1, 47 | "text/xml": 1, 48 | "text/xul": 1, 49 | "application/rdf+xml": 1, 50 | }; 51 | 52 | const unknownTypes = { 53 | "text/xsl": 1, 54 | "text/sgml": 1, 55 | "text/rtf": 1, 56 | "text/x-setext": 1, 57 | "text/richtext": 1, 58 | "text/tab-separated-values": 1, 59 | "text/rdf": 1, 60 | "text/xif": 1, 61 | "text/ecmascript": 1, 62 | "text/vnd.curl": 1, 63 | "text/vbscript": 1, 64 | "view-source": 1, 65 | "view-fragment": 1, 66 | "application/x-httpd-php": 1, 67 | "application/ecmascript": 1, 68 | "application/http-index-format": 1, 69 | }; 70 | 71 | class ContentPie extends Component { 72 | title = "Summary of content types." 73 | 74 | data = [ 75 | { 76 | count: 0, 77 | value: 0, 78 | label: Strings.pieLabelHTMLText, 79 | color: "rgb(174, 234, 218)", 80 | }, 81 | { 82 | count: 0, 83 | value: 0, 84 | label: Strings.pieLabelJavaScript, 85 | color: "rgb(245, 230, 186)", 86 | }, 87 | { 88 | count: 0, 89 | value: 0, 90 | label: Strings.pieLabelCSS, 91 | color: "rgb(212, 204, 219)", 92 | }, 93 | { 94 | count: 0, 95 | value: 0, 96 | label: Strings.pieLabelImage, 97 | color: "rgb(220, 171, 181)", 98 | }, 99 | { 100 | count: 0, 101 | value: 0, 102 | label: Strings.pieLabelFlash, 103 | color: "rgb(166, 156, 222)", 104 | }, 105 | { 106 | count: 0, 107 | value: 0, 108 | label: Strings.pieLabelOthers, 109 | color: "rgb(229, 171, 255)", 110 | }, 111 | ] 112 | 113 | getLabelTooltipText = (item) => { 114 | return item.count + "x " + item.label + ": " + Str.formatSize(item.value); 115 | } 116 | 117 | calcData() { 118 | // Iterate over all requests and compute stats. 119 | const { entries = [] } = this.props; 120 | const data = this.data.map((d) => ({ ...d })); 121 | entries.forEach((entry) => { 122 | if (!entry.timings) { 123 | return; 124 | } 125 | 126 | const { response } = entry; 127 | // TODO use transferred size 128 | const resBodySize = response.bodySize > 0 ? response.bodySize : 0; 129 | 130 | // Get Content type info. Make sure we read the right content type 131 | // even if there is also a charset specified. 132 | const mimeType = Mime.extractMimeType(response.content.mimeType); 133 | 134 | // Collect response sizes according to the contentType. 135 | if (htmlTypes[mimeType]) { 136 | data[0].value += resBodySize; 137 | data[0].count++; 138 | } else if (jsTypes[mimeType]) { 139 | data[1].value += resBodySize; 140 | data[1].count++; 141 | } else if (cssTypes[mimeType]) { 142 | data[2].value += resBodySize; 143 | data[2].count++; 144 | } else if (imageTypes[mimeType]) { 145 | data[3].value += resBodySize; 146 | data[3].count++; 147 | } else if (flashTypes[mimeType]) { 148 | data[4].value += resBodySize; 149 | data[4].count++; 150 | } else { 151 | data[5].value += resBodySize; 152 | data[5].count++; 153 | } 154 | }); 155 | return data; 156 | } 157 | 158 | render() { 159 | return ; 160 | } 161 | } 162 | 163 | ContentPie.propTypes = { 164 | entries: PropTypes.array, 165 | }; 166 | 167 | export default ContentPie; 168 | -------------------------------------------------------------------------------- /webapp/modules/pie/TimingPie.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Pie from "./Pie"; 5 | import * as Str from "../core/string"; 6 | import Strings from "i18n!../nls/harStats"; 7 | 8 | class TimingPie extends Component { 9 | title = "Summary of request times." 10 | 11 | data = [ 12 | { 13 | value: 0, 14 | label: Strings.pieLabelBlocked, 15 | color: "rgb(228, 214, 193)", 16 | }, 17 | { 18 | value: 0, 19 | label: Strings.pieLabelDNS, 20 | color: "rgb(119, 192, 203)", 21 | }, 22 | { 23 | value: 0, 24 | label: Strings.pieLabelSSL, 25 | color: "rgb(168, 196, 173)", 26 | }, 27 | { 28 | value: 0, 29 | label: Strings.pieLabelConnect, 30 | color: "rgb(179, 222, 93)", 31 | }, 32 | { 33 | value: 0, 34 | label: Strings.pieLabelSend, 35 | color: "rgb(224, 171, 157)", 36 | }, 37 | { 38 | value: 0, 39 | label: Strings.pieLabelWait, 40 | color: "rgb(163, 150, 190)", 41 | }, 42 | { 43 | value: 0, 44 | label: Strings.pieLabelReceive, 45 | color: "rgb(194, 194, 194)", 46 | }, 47 | ] 48 | 49 | getLabelTooltipText = (item) => { 50 | return item.label + ": " + Str.formatTime(item.value.toFixed(2)); 51 | } 52 | 53 | calcData() { 54 | // Iterate over all requests and compute stats. 55 | const { entries = [] } = this.props; 56 | const data = this.data.map((d) => ({ ...d })); 57 | entries.forEach((entry) => { 58 | if (!entry.timings) { 59 | return; 60 | } 61 | 62 | // Get timing info (SSL is new in HAR 1.2) 63 | data[0].value += entry.timings.blocked; 64 | data[1].value += entry.timings.dns; 65 | data[2].value += entry.timings.ssl > 0 ? entry.timings.ssl : 0; 66 | data[3].value += entry.timings.connect; 67 | data[4].value += entry.timings.send; 68 | data[5].value += entry.timings.wait; 69 | data[6].value += entry.timings.receive; 70 | 71 | // The ssl time is also included in the connect field, see HAR 1.2 spec 72 | // (to ensure backward compatibility with HAR 1.1). 73 | if (entry.timings.ssl > 0) { 74 | data[3].value -= entry.timings.ssl; 75 | } 76 | }); 77 | return data; 78 | } 79 | 80 | render() { 81 | return ; 82 | } 83 | } 84 | 85 | TimingPie.propTypes = { 86 | entries: PropTypes.array, 87 | }; 88 | 89 | export default TimingPie; 90 | -------------------------------------------------------------------------------- /webapp/modules/pie/TrafficPie.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Pie from "./Pie"; 5 | import * as Str from "../core/string"; 6 | import Strings from "i18n!../nls/harStats"; 7 | 8 | class TrafficPie extends Component { 9 | title = "Summary of sent and received bodies & headers." 10 | 11 | data = [ 12 | { 13 | value: 0, 14 | label: Strings.pieLabelHeadersSent, 15 | color: "rgb(247, 179, 227)", 16 | }, 17 | { 18 | value: 0, 19 | label: Strings.pieLabelBodiesSent, 20 | color: "rgb(226, 160, 241)", 21 | }, 22 | { 23 | value: 0, 24 | label: Strings.pieLabelHeadersReceived, 25 | color: "rgb(166, 232, 166)", 26 | }, 27 | { 28 | value: 0, 29 | label: Strings.pieLabelBodiesReceived, 30 | color: "rgb(168, 196, 173)", 31 | }, 32 | ] 33 | 34 | getLabelTooltipText = (item) => { 35 | return item.label + ": " + Str.formatSize(item.value); 36 | } 37 | 38 | calcData() { 39 | // Iterate over all requests and compute stats. 40 | const { entries = [] } = this.props; 41 | const data = this.data.map((d) => ({ ...d })); 42 | entries.forEach((entry) => { 43 | if (!entry.timings) { 44 | return; 45 | } 46 | 47 | const { response } = entry; 48 | // TODO use transferred size 49 | const resBodySize = response.bodySize > 0 ? response.bodySize : 0; 50 | 51 | // Get traffic info 52 | data[0].value += entry.request.headersSize > 0 ? entry.request.headersSize : 0; 53 | data[1].value += entry.request.bodySize > 0 ? entry.request.bodySize : 0; 54 | data[2].value += entry.response.headersSize > 0 ? entry.response.headersSize : 0; 55 | data[3].value += resBodySize; 56 | }); 57 | return data; 58 | } 59 | 60 | render() { 61 | return ; 62 | } 63 | } 64 | 65 | TrafficPie.propTypes = { 66 | entries: PropTypes.array, 67 | }; 68 | 69 | export default TrafficPie; 70 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/DataURL.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Str from "../core/string"; 5 | 6 | class DataURL extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.domRef = React.createRef(); 10 | } 11 | 12 | handleImage() { } 13 | 14 | componentDidMount() { } 15 | 16 | render() { 17 | const { entry } = this.props; 18 | const data = entry.request.url; 19 | 20 | const content = (data.indexOf("data:image") === 0) ? 21 | : 22 |
;
23 | 
24 |     return (
25 |       
26 | {content} 27 |
28 | ); 29 | } 30 | }; 31 | 32 | DataURL.propTypes = { 33 | entry: PropTypes.object, 34 | }; 35 | 36 | DataURL.canShowEntry = function(entry) { 37 | return entry.request.url.indexOf("data:") === 0; 38 | }; 39 | 40 | export default DataURL; 41 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/ExternalImage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Image from "./Image"; 5 | 6 | class ExternalImage extends React.Component { 7 | render() { 8 | const { entry } = this.props; 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | }; 16 | 17 | ExternalImage.propTypes = { 18 | entry: PropTypes.object, 19 | }; 20 | 21 | ExternalImage.canShowEntry = function(entry) { 22 | return Image.isFileImage(entry); 23 | }; 24 | 25 | export default ExternalImage; 26 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/Headers.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ParamRow from "./ParamRow"; 5 | import TitleRow from "./TitleRow"; 6 | 7 | import * as Str from "../core/string"; 8 | 9 | class Headers extends React.Component { 10 | render() { 11 | const { entry } = this.props; 12 | 13 | const responseHeaders = entry.response.headers || []; 14 | const requestHeaders = entry.request.headers || []; 15 | 16 | return ( 17 |
18 | {Str.cropString(entry.request.url, 128)} 19 | 20 | 21 | 22 | 23 | 24 | { 25 | responseHeaders.map((header, i) => 26 | 27 | ) 28 | } 29 | 30 | 31 | 32 | { 33 | requestHeaders.map((header, i) => 34 | 35 | ) 36 | } 37 | 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | Headers.canShowEntry = function(entry) { 45 | return true; 46 | }; 47 | 48 | Headers.propTypes = { 49 | entry: PropTypes.object, 50 | }; 51 | 52 | export default Headers; 53 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/Highlighted.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Mime from "../core/mime"; 5 | import { canDecode } from "./decoder"; 6 | 7 | class Highlighted extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.domRef = React.createRef(); 11 | } 12 | 13 | maybeDoHighlighting() { 14 | const { entry } = this.props; 15 | const text = entry.response.content.text; 16 | const brush = Highlighted.shouldHighlightAs(entry.response.content.mimeType); 17 | 18 | const pre = this.domRef.current; 19 | const code = pre.firstChild; 20 | 21 | // clear flag 22 | code.removeAttribute("highlighted"); 23 | 24 | code.innerText = ""; 25 | code.appendChild(document.createTextNode(text)); 26 | 27 | if (brush) { 28 | code.className = brush; 29 | // Run highlightElement on the , not the
 30 |       hljs.highlightBlock(code);
 31 | 
 32 |       // test that highlighting has worked, and set a flag that helps with testing.
 33 |       const highlightedElement = code;
 34 |       if (code.classList.contains("hljs")) {
 35 |         highlightedElement.setAttribute("highlighted", true);
 36 |       }
 37 |     }
 38 |   }
 39 | 
 40 |   componentDidMount() {
 41 |     this.maybeDoHighlighting();
 42 |   }
 43 | 
 44 |   componentDidUpdate() {
 45 |     this.maybeDoHighlighting();
 46 |   }
 47 | 
 48 |   render() {
 49 |     // highlightjs needs a  block inside a 
 block.
 50 |     return (
 51 |       
52 |
 53 |           
 54 |         
55 |
56 | ); 57 | } 58 | }; 59 | 60 | Highlighted.propTypes = { 61 | entry: PropTypes.object, 62 | }; 63 | 64 | Highlighted.canShowEntry = function(entry) { 65 | const { content } = entry.response; 66 | 67 | if (!content || !content.text) { 68 | return false; 69 | } 70 | 71 | if (!canDecode(content.encoding)) { 72 | return false; 73 | } 74 | 75 | // Remove any mime type parameters (if any) 76 | const mimeType = Mime.extractMimeType(content.mimeType || ""); 77 | 78 | const shouldHighlightAs = Highlighted.shouldHighlightAs(mimeType); 79 | return (shouldHighlightAs !== null); 80 | }; 81 | 82 | // TODO - Put this in the right place when we implement XML tab. 83 | const XmlTab = {}; 84 | 85 | XmlTab.isXmlMimeType = function(mimeType) { 86 | mimeType = Mime.extractMimeType(mimeType); 87 | return [ 88 | "text/xml", 89 | "application/xml", 90 | "image/svg+xml", 91 | "application/atom+xml", 92 | "application/xslt+xml", 93 | "application/mathml+xml", 94 | "application/rss+xml", 95 | ].indexOf(mimeType) > -1; 96 | }; 97 | 98 | Highlighted.shouldHighlightAs = function(mimeType) { 99 | const mimeTypesToHighlight = { 100 | javascript: [ 101 | "application/javascript", 102 | "text/javascript", 103 | "application/x-javascript", 104 | "text/ecmascript", 105 | "application/ecmascript", 106 | "application/json", 107 | ], 108 | css: ["text/css"], 109 | html: ["text/html", "application/xhtml+xml"], 110 | }; 111 | for (const brush in mimeTypesToHighlight) { 112 | if (mimeTypesToHighlight[brush].indexOf(mimeType) > -1) { 113 | return brush; 114 | } 115 | } 116 | return XmlTab.isXmlMimeType(mimeType) ? "xml" : null; 117 | }; 118 | 119 | export default Highlighted; 120 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/Image.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Mime from "../core/mime"; 5 | import * as Str from "../core/string"; 6 | 7 | class Image extends React.Component { 8 | createImageSrc(content) { 9 | const mimeType = Mime.extractMimeType(content.mimeType); 10 | // https://css-tricks.com/data-uris/ 11 | return "data:" + mimeType + ";base64," + content.text; 12 | } 13 | 14 | render() { 15 | const { entry } = this.props; 16 | const { content } = entry.response; 17 | const src = this.createImageSrc(content); 18 | return ( 19 |
20 | 21 |
22 | ); 23 | } 24 | }; 25 | 26 | Image.propTypes = { 27 | entry: PropTypes.object, 28 | }; 29 | 30 | Image.isFileImage = function(entry) { 31 | const { content } = entry.response; 32 | if (!content) { 33 | return false; 34 | } 35 | 36 | const mimeType = Mime.extractMimeType(content.mimeType || ""); 37 | return Str.startsWith(mimeType, "image/"); 38 | } 39 | 40 | Image.canShowEntry = function(entry) { 41 | const { content } = entry.response; 42 | return Image.isFileImage(entry) && (content.text && content.encoding === "base64"); 43 | }; 44 | 45 | export default Image; 46 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/JSONEntryTree.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ObjectTree, { hasChildren, populateChildren } from "../tree/ObjectTree"; 5 | import { canDecode, decode } from "./decoder"; 6 | import * as Mime from "../core/mime"; 7 | 8 | function getRoot(entry) { 9 | const { content } = entry.response; 10 | const jsonStr = decode(content.text, content.encoding); 11 | return JSON.parse(jsonStr); 12 | } 13 | 14 | class JSONEntryTree extends React.Component { 15 | render() { 16 | const { entry } = this.props; 17 | const root = getRoot(entry); 18 | return ; 19 | } 20 | }; 21 | 22 | JSONEntryTree.propTypes = { 23 | entry: PropTypes.object, 24 | }; 25 | 26 | JSONEntryTree.canShowEntry = function(entry) { 27 | const { content } = entry.response; 28 | 29 | if (!content || !content.text) { 30 | return false; 31 | } 32 | 33 | if (!canDecode(content.encoding)) { 34 | return false; 35 | } 36 | 37 | const mimeType = Mime.extractMimeType(content.mimeType || ""); 38 | return ["application/json"].indexOf(mimeType) > -1; 39 | }; 40 | 41 | export default JSONEntryTree; 42 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/ParamRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Str from "../core/string"; 5 | 6 | function getParamValue(value) { 7 | // This value is inserted into PRE element and so, make sure the HTML isn't escaped (1210). 8 | // This is why the second parameter is true. 9 | // The PRE element preserves whitespaces so they are displayed the same, as they come from 10 | // the server (1194). 11 | return Str.wrapText(value, true); 12 | } 13 | 14 | const ParamRow = (props) => { 15 | return ( 16 | 17 | {props.name} 18 |
{getParamValue(props.value)}
19 | 20 | ); 21 | }; 22 | 23 | ParamRow.propTypes = { 24 | name: PropTypes.string, 25 | value: PropTypes.string, 26 | }; 27 | 28 | export default ParamRow; 29 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/PlainResponse.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Str from "../core/string"; 5 | 6 | class PlainResponse extends React.Component { 7 | render() { 8 | const { entry } = this.props; 9 | const { text } = entry.response.content; 10 | 11 | return ( 12 |
13 |
14 |           {Str.wrapText(text, true)}
15 |         
16 |
17 | ); 18 | } 19 | } 20 | 21 | PlainResponse.canShowEntry = function(entry) { 22 | return (entry.response.content.text && entry.response.content.text.length > 0); 23 | }; 24 | 25 | PlainResponse.propTypes = { 26 | entry: PropTypes.object, 27 | }; 28 | 29 | export default PlainResponse; 30 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/SendData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import * as Arr from "../core/array"; 5 | import ParamRow from "./ParamRow"; 6 | 7 | class SendData extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | const { entry } = this.props; 14 | const { postData } = entry.request; 15 | if (!postData) { 16 | return null; 17 | } 18 | 19 | let content = null; 20 | if (postData.mimeType === "application/x-www-form-urlencoded") { 21 | content = ( 22 | 23 | 24 | {postData.params.map((param, i) => )} 25 | 26 |
27 | ); 28 | } else { 29 | // TODO 30 | content = postData.text; 31 | } 32 | return ( 33 |
34 | {content} 35 |
36 | ); 37 | } 38 | }; 39 | 40 | SendData.propTypes = { 41 | entry: PropTypes.object, 42 | }; 43 | 44 | SendData.canShowEntry = function(entry) { 45 | const { postData } = entry.request; 46 | if (!postData) { 47 | // No post data at all. 48 | return false; 49 | } 50 | 51 | const paramsMissing = !Arr.isArray(postData.params) || (postData.params.length === 0); 52 | const textMissing = !postData.text; 53 | 54 | const postContentMissing = paramsMissing && textMissing; 55 | 56 | if (postContentMissing) { 57 | // (at least) Firefox 47 exports GET requests in HARs that have: 58 | // postData.mimeType = "" 59 | // postData.params = [] 60 | // postData.text = "" 61 | // So double-check for this and only allow such 'empty' postData for PUT and POST 62 | return ["PUT", "POST"].indexOf(entry.request.method) > -1; 63 | } 64 | 65 | return true; 66 | }; 67 | 68 | export default SendData; 69 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/TitleRow.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const TitleRow = (props) => { 5 | return ( 6 | 7 |
{props.title}
8 | 9 | ); 10 | }; 11 | 12 | TitleRow.propTypes = { 13 | title: PropTypes.string, 14 | titleType: PropTypes.string, 15 | }; 16 | 17 | export default TitleRow; 18 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/URLParameters.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ParamRow from "./ParamRow"; 5 | 6 | class UrlParameters extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | } 10 | 11 | render() { 12 | const { entry } = this.props; 13 | return ( 14 | 15 | 16 | { 17 | entry.request.queryString.map((param, i) => 18 | 19 | ) 20 | } 21 | 22 |
23 | ); 24 | } 25 | }; 26 | 27 | UrlParameters.propTypes = { 28 | entry: PropTypes.object, 29 | }; 30 | 31 | UrlParameters.canShowEntry = function(entry) { 32 | return (entry.request.queryString && entry.request.queryString.length); 33 | }; 34 | 35 | export default UrlParameters; 36 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/XMLEntryTree.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import XMLTree, { hasChildren, populateChildren } from "../tree/XMLTree"; 5 | import { canDecode, decode } from "./decoder"; 6 | import * as Mime from "../core/mime"; 7 | 8 | function getRoot(entry) { 9 | const { content } = entry.response; 10 | const xmlStr = decode(content.text, content.encoding); 11 | return $.parseXML(xmlStr); 12 | } 13 | 14 | class XMLEntryTree extends React.Component { 15 | render() { 16 | const { entry } = this.props; 17 | const root = getRoot(entry); 18 | return ; 19 | } 20 | }; 21 | 22 | XMLEntryTree.propTypes = { 23 | entry: PropTypes.object, 24 | }; 25 | 26 | XMLEntryTree.isXmlMimeType = function(mimeType) { 27 | mimeType = Mime.extractMimeType(mimeType); 28 | return [ 29 | "text/xml", 30 | "application/xml", 31 | "image/svg+xml", 32 | "application/atom+xml", 33 | "application/xslt+xml", 34 | "application/mathml+xml", 35 | "application/rss+xml", 36 | ].indexOf(mimeType) > -1; 37 | }; 38 | 39 | XMLEntryTree.canShowEntry = function(entry) { 40 | const { content } = entry.response; 41 | 42 | if (!content || !content.text) { 43 | return false; 44 | } 45 | 46 | if (!canDecode(content.encoding)) { 47 | return false; 48 | } 49 | 50 | const mimeType = Mime.extractMimeType(content.mimeType || ""); 51 | return XMLEntryTree.isXmlMimeType(mimeType); 52 | }; 53 | 54 | export default XMLEntryTree; 55 | -------------------------------------------------------------------------------- /webapp/modules/requestbodies/decoder.js: -------------------------------------------------------------------------------- 1 | export function canDecode(encoding) { 2 | if (!encoding) { 3 | return true; 4 | } 5 | 6 | if (encoding !== "base64") { 7 | // only base64 supported 8 | return false; 9 | } 10 | 11 | if (typeof atob === "undefined") { 12 | // it's base64 but we can't decode it. E.g. IE9. 13 | return false; 14 | } 15 | 16 | return true; 17 | } 18 | 19 | export function decode(text, encoding) { 20 | return (encoding === "base64") ? atob(text) : text; 21 | } 22 | -------------------------------------------------------------------------------- /webapp/modules/setState.js: -------------------------------------------------------------------------------- 1 | export default function setState(context, ...sources) { 2 | context.setState((prevState) => { 3 | const newState = Object.assign({}, context.state, ...sources); 4 | return newState; 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /webapp/modules/tabs/AboutTab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import aboutHtml from "raw-loader!./aboutTab.html"; 5 | 6 | class AboutTab extends Component { 7 | replace(s, pattern, replaceWith) { 8 | if (!replaceWith) { 9 | return s; 10 | } 11 | return s.replace(new RegExp(pattern, "g"), replaceWith); 12 | } 13 | 14 | render() { 15 | let { version, harSpecURL, harViewerDemoUrl } = this.props; 16 | if (!harSpecURL) { 17 | harSpecURL = "http://www.softwareishard.com/blog/har-12-spec/"; 18 | } 19 | 20 | let html = aboutHtml; 21 | html = this.replace(html, "@VERSION@", version); 22 | html = this.replace(html, "@HAR_SPEC_URL@", harSpecURL); 23 | html = this.replace(html, "@HARVIEWER_DEMO_URL@", harViewerDemoUrl); 24 | 25 | return ( 26 |
27 | ); 28 | } 29 | } 30 | 31 | AboutTab.propTypes = { 32 | version: PropTypes.string, 33 | harSpecURL: PropTypes.string, 34 | harViewerDemoUrl: PropTypes.string, 35 | }; 36 | 37 | export default AboutTab; 38 | -------------------------------------------------------------------------------- /webapp/modules/tabs/HomeTab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import AppContext from "../AppContext"; 5 | import homeHtml from "raw-loader!./homeTab.html"; 6 | 7 | // TODO Move me 8 | if (!Element.prototype.matches) { 9 | Element.prototype.matches = Element.prototype.msMatchesSelector; 10 | } 11 | 12 | function on(e, selector, listener) { 13 | const { target } = e; 14 | if (target.matches(selector)) { 15 | listener.call(null, e); 16 | } 17 | } 18 | 19 | export class HomeTab extends Component { 20 | constructor(props) { 21 | super(props); 22 | this.ref = React.createRef(); 23 | } 24 | 25 | componentDidMount() { 26 | this.validate = this.ref.current.querySelector("#validate"); 27 | this.validate.checked = this.context.validate; 28 | } 29 | 30 | componentWillUnmount() { 31 | this.validate = null; 32 | } 33 | 34 | componentDidUpdate(prevProps, prevState, snapshot) { 35 | this.validate.checked = this.context.validate; 36 | } 37 | 38 | onClick = (e) => { 39 | on(e, ".example", this.onExampleClick); 40 | on(e, ".linkAbout", this.onAboutClick); 41 | on(e, "#appendPreview", this.onPreviewClick); 42 | on(e, "#validate", this.onValidateClick); 43 | } 44 | 45 | onExampleClick = (e) => { 46 | e.preventDefault(); 47 | const { target } = e; 48 | const har = target.getAttribute("har"); 49 | const href = window.location.href; 50 | const page = href.split("?")[0]; 51 | window.location = page + "?har=" + har; 52 | } 53 | 54 | onAboutClick = (e) => { 55 | e.preventDefault(); 56 | this.props.requestTabChange("About"); 57 | } 58 | 59 | onPreviewClick = (e) => { 60 | const json = document.getElementById("sourceEditor").value; 61 | const { appendPreview } = this.props; 62 | appendPreview(json); 63 | } 64 | 65 | onValidateClick = (e) => { 66 | e.stopPropagation(); 67 | const checked = e.target.checked; 68 | const { setValidate } = this.context; 69 | setValidate(checked); 70 | } 71 | 72 | render() { 73 | return ( 74 |
80 | ); 81 | } 82 | }; 83 | 84 | HomeTab.propTypes = { 85 | appendPreview: PropTypes.func, 86 | requestTabChange: PropTypes.func, 87 | }; 88 | 89 | HomeTab.contextType = AppContext; 90 | 91 | export default HomeTab; 92 | -------------------------------------------------------------------------------- /webapp/modules/tabs/PreviewTab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { saveAs } from "file-saver"; 4 | 5 | import AppContext from "../AppContext"; 6 | import Stats from "../Stats"; 7 | import PageTimeline from "../pagetimeline/PageTimeline"; 8 | import PreviewTabToolbar from "./PreviewTabToolbar"; 9 | import PreviewList from "./preview/PreviewList"; 10 | 11 | class PreviewTab extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.toolbarRef = React.createRef(); 16 | 17 | this.state = { 18 | timelineVisible: false, 19 | selectedPages: null, 20 | statsVisible: false, 21 | }; 22 | } 23 | 24 | showStats() { 25 | this.setState(({ statsVisible }) => ({ 26 | statsVisible: !statsVisible, 27 | })); 28 | } 29 | 30 | showTimeline() { 31 | this.setState(({ timelineVisible }) => ({ 32 | timelineVisible: !timelineVisible, 33 | })); 34 | } 35 | 36 | addPageTiming(timing) { 37 | this.context.addPageTiming(timing); 38 | } 39 | 40 | onDownloadClick = (e) => { 41 | e.preventDefault(); 42 | // TODO find out which model to copy. 43 | const { harModels } = this.props; 44 | const model = harModels[0]; 45 | const json = model ? model.toJSON() : ""; 46 | const blob = new Blob([json], { type: "text/plain;charset=" + document.characterSet }); 47 | saveAs(blob, "netData.har"); 48 | } 49 | 50 | onStatsClick = (e) => { 51 | e.preventDefault(); 52 | this.showStats(); 53 | } 54 | 55 | onTimelineClick = (e) => { 56 | e.preventDefault(); 57 | this.showTimeline(); 58 | } 59 | 60 | onClearClick = (e) => { 61 | e.preventDefault(); 62 | const href = window.location.href; 63 | const index = href.indexOf("?"); 64 | window.location = href.substr(0, index); 65 | } 66 | 67 | findPagelessEntries(har) { 68 | const { pages, entries } = har.log; 69 | 70 | let pageIds = {}; 71 | if (pages && pages.length > 0) { 72 | pageIds = pages.reduce((ids, page) => { 73 | if (page.id) { 74 | ids[page.id] = 1; 75 | } 76 | return ids; 77 | }, {}); 78 | } else { 79 | // No pages, so all entries are pageless 80 | return entries; 81 | } 82 | 83 | if (entries && entries.length > 0) { 84 | return entries.filter((e) => { 85 | if (!e.pageref) { 86 | // pageless 87 | return true; 88 | } 89 | // pageless if there isn't a matching page.id 90 | return !pageIds || !pageIds[e.pageref]; 91 | }); 92 | } 93 | 94 | return null; 95 | } 96 | 97 | getToolbar() { 98 | return this.toolbarRef.current; 99 | } 100 | 101 | onPageTimelinePageSelection = (selectedPages) => { 102 | this.setState({ 103 | selectedPages, 104 | }); 105 | } 106 | 107 | render() { 108 | const { harModels, errors } = this.props; 109 | 110 | if (!harModels) { 111 | return
; 112 | } 113 | 114 | // TODO - how to choose which model? 115 | const clickHandlers = { 116 | onStatsClick: this.onStatsClick, 117 | onTimelineClick: this.onTimelineClick, 118 | onClearClick: this.onClearClick, 119 | onDownloadClick: this.onDownloadClick, 120 | }; 121 | 122 | // TODO - Stats currently only takes one model (harviewer does it this way) 123 | // harviewer-react has an array of models though 124 | 125 | const { selectedPages } = this.state; 126 | const statsEntries = selectedPages 127 | ? selectedPages.reduce((result, { harModel, page }) => result.concat(harModel.getPageEntries(page)), []) 128 | : harModels.reduce((result, harModel) => result.concat(harModel.getAllEntries()), []); 129 | 130 | return ( 131 |
132 |
133 | 134 |
135 |
136 | {this.state.timelineVisible ? : ""} 137 |
138 |
139 | {this.state.statsVisible ? : ""} 140 |
141 | 142 |
143 | ); 144 | } 145 | }; 146 | 147 | PreviewTab.propTypes = { 148 | harModels: PropTypes.array, 149 | errors: PropTypes.array, 150 | }; 151 | 152 | PreviewTab.contextType = AppContext; 153 | 154 | export default PreviewTab; 155 | -------------------------------------------------------------------------------- /webapp/modules/tabs/PreviewTabToolbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Toolbar, { Button } from "../toolbar/Toolbar"; 5 | 6 | class PreviewTabToolbar extends Component { 7 | state = { 8 | showButtons: ["showTimeline", "showStats", "clear", "download"], 9 | } 10 | 11 | removeButton(buttonName) { 12 | this.setState(({ showButtons }) => ({ 13 | showButtons: showButtons.filter((it) => it !== buttonName), 14 | })); 15 | } 16 | 17 | render() { 18 | const { 19 | onStatsClick, 20 | onTimelineClick, 21 | onClearClick, 22 | onDownloadClick, 23 | } = this.props; 24 | const { showButtons } = this.state; 25 | 26 | if (showButtons.length === 0) { 27 | return null; 28 | } 29 | 30 | const downloadImgStyle = { 31 | display: "inline-block", 32 | width: "16px", 33 | height: "16px", 34 | background: "url(./css/images/download-sprites.png) no-repeat", 35 | }; 36 | 37 | const buttons = { 38 | showTimeline: , 39 | showStats: , 40 | clear: , 41 | download: , 44 | }; 45 | 46 | return ( 47 | 48 | {showButtons.map((buttonName) => buttons[buttonName])} 49 | 50 | ); 51 | } 52 | }; 53 | 54 | PreviewTabToolbar.propTypes = { 55 | onStatsClick: PropTypes.func, 56 | onTimelineClick: PropTypes.func, 57 | onClearClick: PropTypes.func, 58 | onDownloadClick: PropTypes.func, 59 | }; 60 | 61 | export default PreviewTabToolbar; 62 | -------------------------------------------------------------------------------- /webapp/modules/tabs/SchemaTab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class SchemaTab extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = {}; 8 | this.domRef = React.createRef(); 9 | } 10 | 11 | maybeDoHighlighting() { 12 | const { text } = this.state; 13 | if (!text) { 14 | return; 15 | } 16 | 17 | const pre = this.domRef.current; 18 | const code = pre.firstChild; 19 | 20 | // clear flag 21 | code.removeAttribute("highlighted"); 22 | 23 | code.innerText = ""; 24 | code.appendChild(document.createTextNode(text)); 25 | 26 | code.className = "javascript"; 27 | // Run highlightElement on the , not the
28 |     hljs.highlightBlock(code);
29 | 
30 |     // test that highlighting has worked, and set a flag that helps with testing.
31 |     const highlightedElement = code;
32 |     if (code.classList.contains("hljs")) {
33 |       highlightedElement.setAttribute("highlighted", true);
34 |     }
35 |   }
36 | 
37 |   componentDidMount() {
38 |     const { text } = this.state;
39 |     if (text) {
40 |       return;
41 |     }
42 |     fetch("modules/preview/harSchema.js")
43 |       .then((response) => response.text())
44 |       .then((text) => {
45 |         const state = { text };
46 |         this.setState(state, () => this.maybeDoHighlighting());
47 |       });
48 |   }
49 | 
50 |   render() {
51 |     const brush = "javascript";
52 |     return (
53 |       
54 |         
55 |       
56 | ); 57 | } 58 | } 59 | 60 | SchemaTab.propTypes = { 61 | version: PropTypes.string, 62 | harSpecURL: PropTypes.string, 63 | harViewerExampleApp: PropTypes.string, 64 | }; 65 | 66 | export default SchemaTab; 67 | -------------------------------------------------------------------------------- /webapp/modules/tabs/dom/DOMBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | import ObjectTree from "../../tree/ObjectTree"; 6 | 7 | function JSONQueryResults({ results }) { 8 | if (!results || results.length === 0) { 9 | return ( 10 |
JSON Query Results
11 | ); 12 | } 13 | return ( 14 | <> 15 |
16 | 17 | Table View 18 |
19 | 20 | 21 | ); 22 | } 23 | 24 | class DOMBox extends Component { 25 | objectTreeRef = React.createRef(); 26 | 27 | getTree() { 28 | return this.objectTreeRef.current.getTree(); 29 | } 30 | 31 | render() { 32 | const { 33 | har, 34 | title, 35 | jsonQueryMode = false, 36 | jsonQueryResults, 37 | } = this.props; 38 | 39 | if (!har) { 40 | return null; 41 | } 42 | 43 | return ( 44 | 45 | 46 | 47 | 51 | { 52 | jsonQueryMode && ( 53 | <> 54 | 55 | 58 | 59 | ) 60 | } 61 | 62 | 63 |
48 |
{title}
49 | 50 |
56 | 57 |
64 | ); 65 | } 66 | } 67 | 68 | DOMBox.propTypes = { 69 | har: PropTypes.object, 70 | title: PropTypes.string, 71 | }; 72 | 73 | export default DOMBox; 74 | -------------------------------------------------------------------------------- /webapp/modules/tabs/dom/DOMTab.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import "../../json-query/JSONQuery"; 5 | 6 | import Dom from "../../core/dom"; 7 | import Cookies from "../../core/cookies"; 8 | import Trace from "../../core/trace"; 9 | import Toolbar from "../../toolbar/Toolbar"; 10 | import ObjectSearch from "../../tabs/ObjectSearch"; 11 | import intersperse from "../../intersperse"; 12 | import DOMBox from "./DOMBox"; 13 | import SearchBox from "./SearchBox"; 14 | 15 | const caseSensitiveCookieName = "searchCaseSensitive"; 16 | 17 | class DOMTab extends Component { 18 | constructor(...args) { 19 | super(...args); 20 | 21 | this.domBoxRefs = this.props.harModels.map(() => React.createRef()); 22 | this.searchRef = React.createRef(); 23 | 24 | this.state = { 25 | jsonQueryResults: null, 26 | }; 27 | } 28 | 29 | async selectText(search) { 30 | const key = search.stack 31 | .slice(1) 32 | .reduce((key, stackItem, i, stack) => { 33 | let propIndex = stackItem.propIndex; 34 | if (i < stack.length - 1) { 35 | // all indexes except the last are off-by-one when it comes to generating the key. 36 | propIndex -= 1; 37 | } 38 | return key ? key + "." + propIndex : String(propIndex); 39 | }, ""); 40 | 41 | 42 | // The root of search data is the list of inputs. 43 | const { propIndex } = search.stack[0]; 44 | 45 | const domBoxRef = this.domBoxRefs[propIndex - 1]; 46 | 47 | // wait for the nodes to be shown before trying to select text 48 | await domBoxRef.current.getTree().showNode(key); 49 | 50 | const objectBox = domBoxRef.current.getTree().findObjectBox(key); 51 | if (objectBox) { 52 | const textNode = objectBox.firstChild; 53 | search.selectText(textNode); 54 | Dom.scrollIntoCenterView(objectBox); 55 | } 56 | } 57 | 58 | search = (text) => { 59 | if (this.isJSONQueryMode()) { 60 | return this.jsonQuery(text); 61 | } 62 | 63 | if (text.length < 3) { 64 | return true; 65 | } 66 | 67 | if (this.currentSearch && this.currentSearch.text !== text) { 68 | this.currentSearch = null; 69 | } 70 | 71 | if (!this.currentSearch) { 72 | const { harModels } = this.props; 73 | const inputs = harModels.map(({ input }) => input); 74 | this.currentSearch = new ObjectSearch(text, inputs, false, caseSensitiveCookieName); 75 | } 76 | 77 | if (this.currentSearch.findNext(text)) { 78 | this.selectText(this.currentSearch); 79 | return true; 80 | } 81 | 82 | this.currentSearch = null; 83 | 84 | return false; 85 | } 86 | 87 | jsonQuery(expr) { 88 | const { harModels } = this.props; 89 | 90 | const jsonQueryResults = harModels.map(({ input, i }) => { 91 | try { 92 | return JSONQuery(expr, input); 93 | } catch (err) { 94 | Trace.exception(err); 95 | } 96 | }); 97 | 98 | this.setState({ 99 | jsonQueryResults, 100 | }); 101 | 102 | return true; 103 | } 104 | 105 | getTitle(har) { 106 | // Iterate all pages and get titles. 107 | // Some IE11 HARs (11.48.17134.0/11.0.65) don't have pages 108 | return (har.log.pages || []) 109 | .map(({ title }) => title) 110 | .join(", "); 111 | } 112 | 113 | isJSONQueryMode() { 114 | return (Cookies.getCookie("searchJsonQuery") === "true"); 115 | } 116 | 117 | renderToolbar() { 118 | const placeholder = this.isJSONQueryMode() ? "JSON Query" : "Search"; 119 | return ( 120 |
121 | 122 | 123 | 124 |
125 | ); 126 | } 127 | 128 | render() { 129 | const { harModels } = this.props; 130 | const { jsonQueryResults } = this.state; 131 | return ( 132 | <> 133 | {this.renderToolbar()} 134 |
135 | { 136 | intersperse( 137 | harModels.map((model, i) => { 138 | const title = this.getTitle(model.input); 139 | return ; 147 | }), 148 | (_, i) =>
149 | ) 150 | } 151 |
152 | 153 | ); 154 | } 155 | } 156 | 157 | DOMTab.propTypes = { 158 | appendPreview: PropTypes.func, 159 | requestTabChange: PropTypes.func, 160 | harModels: PropTypes.array, 161 | }; 162 | 163 | export default DOMTab; 164 | -------------------------------------------------------------------------------- /webapp/modules/tabs/dom/SearchBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class SearchBox extends Component { 5 | state = { 6 | searchValue: "", 7 | status: null, 8 | }; 9 | 10 | onChange = (e) => { 11 | const text = e.target.value; 12 | this.setState({ 13 | searchValue: text, 14 | }); 15 | } 16 | 17 | onKeyPress = (e) => { 18 | if (e.key === "Enter") { 19 | e.preventDefault(); 20 | this.search(e.target.value); 21 | } 22 | } 23 | 24 | search(text) { 25 | const result = this.props.search(text); 26 | this.setState({ 27 | status: result ? null : "notfound", 28 | }); 29 | } 30 | 31 | render() { 32 | const { status } = this.state; 33 | const { placeholder = "Search" } = this.props; 34 | return ( 35 | 36 |   37 | 38 | 47 |   48 | 49 | 50 | ); 51 | } 52 | } 53 | 54 | SearchBox.propTypes = { 55 | search: PropTypes.func, 56 | placeholder: PropTypes.string, 57 | }; 58 | 59 | export default SearchBox; 60 | -------------------------------------------------------------------------------- /webapp/modules/tabs/homeTab.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • Paste HAR 4 | log into the text box below and 5 | press the Preview button.
  • 6 |
  • Or drop *.har file(s) anywhere on the page (if your browser supports that).
  • 7 |
8 | 9 | 10 | 11 | 12 | 13 |
Validate data before processing?
14 | 15 |

16 | 17 | 18 | 19 |

20 |
21 |

HAR Log Examples

22 |
    23 |
  • 24 | Inline scripts block - Inline scripts block the page load.
  • 25 |
  • 26 | Blocking time - Impact of a limit of max number of parallel connections.
  • 27 |
  • 28 | Browser cache - Impact of the browser cache on page load (the same page loaded three times).
  • 29 |
  • 30 | Single page - Single page load (empty cache).
  • 31 |
  • Embedding HAR Viewer - into other pages.
  • 32 |
33 |

HAR Logs Online

34 | For all the ways you can load HAR files into HAR Viewer, see the About tab. 35 | 36 |
37 |

This viewer supports HAR 1.2 (see the About tab).

38 |
39 | -------------------------------------------------------------------------------- /webapp/modules/tabs/preview/PreviewList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import ValidationError from "./ValidationError"; 5 | import NetTable from "../../nettable/NetTable"; 6 | import PageTable from "../../pagetable/PageTable"; 7 | 8 | import AppContext from "../../AppContext"; 9 | 10 | class PreviewList extends React.Component { 11 | findPagelessEntries(har) { 12 | const { pages, entries } = har.log; 13 | 14 | let pageIds = {}; 15 | if (pages && pages.length > 0) { 16 | pageIds = pages.reduce((ids, page) => { 17 | if (page.id) { 18 | ids[page.id] = 1; 19 | } 20 | return ids; 21 | }, {}); 22 | } else { 23 | // No pages, so all entries are pageless 24 | return entries; 25 | } 26 | 27 | if (entries && entries.length > 0) { 28 | return entries.filter((e) => { 29 | if (!e.pageref) { 30 | // pageless 31 | return true; 32 | } 33 | // pageless if there isn't a matching page.id 34 | return !pageIds || !pageIds[e.pageref]; 35 | }); 36 | } 37 | 38 | return null; 39 | } 40 | 41 | render() { 42 | const { harModels, errors } = this.props; 43 | const { expandAll } = this.context; 44 | 45 | return ( 46 |
47 | { 48 | harModels.map((model, i) => { 49 | const pageTable = ; 50 | 51 | // If there are pageless entries in the HAR, show them in a standalone NetTable 52 | const pagelessEntries = this.findPagelessEntries(model.input); 53 | if (pagelessEntries && pagelessEntries.length > 0) { 54 | const netTable = ; 55 | return [netTable, pageTable]; 56 | } 57 | 58 | return pageTable; 59 | }) 60 | } 61 | { 62 | (errors || []).map((error, i) => ) 63 | } 64 |
65 | ); 66 | } 67 | } 68 | 69 | PreviewList.propTypes = { 70 | harModels: PropTypes.array, 71 | errors: PropTypes.array, 72 | }; 73 | 74 | PreviewList.contextType = AppContext; 75 | 76 | export default PreviewList; 77 | -------------------------------------------------------------------------------- /webapp/modules/tabs/preview/ValidationError.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class ValidationError extends React.Component { 5 | render() { 6 | const { error } = this.props; 7 | if (!error) { 8 | return null; 9 | } 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
{error.property}
 
 {error.message}
21 | ); 22 | } 23 | } 24 | 25 | ValidationError.propTypes = { 26 | error: PropTypes.object, 27 | }; 28 | 29 | export default ValidationError; 30 | -------------------------------------------------------------------------------- /webapp/modules/tabview/Tab.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class Tab extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.linkRef = React.createRef(); 8 | } 9 | 10 | updateSelected() { 11 | const { id, selected } = this.props; 12 | this.linkRef.current.setAttribute("selected", selected); 13 | this.linkRef.current.setAttribute("view", id); 14 | } 15 | 16 | componentDidMount() { 17 | this.updateSelected(); 18 | } 19 | 20 | componentDidUpdate() { 21 | this.updateSelected(); 22 | } 23 | 24 | render() { 25 | const { title, id, label, content, onSelect, selected } = this.props; 26 | return ( 27 | 28 | {content || label || id} 29 | 30 | ); 31 | } 32 | }; 33 | 34 | Tab.propTypes = { 35 | content: PropTypes.node, 36 | id: PropTypes.string, 37 | label: PropTypes.string, 38 | onSelect: PropTypes.func, 39 | selected: PropTypes.bool, 40 | title: PropTypes.string, 41 | }; 42 | 43 | export default Tab; 44 | -------------------------------------------------------------------------------- /webapp/modules/tabview/TabBar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Tab from "./Tab"; 5 | 6 | class TabBar extends React.Component { 7 | onSelectTab(tabIdx, tab) { 8 | const { tabs, onSelectTab } = this.props; 9 | if (onSelectTab) { 10 | onSelectTab(tabIdx, tabs[tabIdx], tabs); 11 | } 12 | } 13 | 14 | render() { 15 | const { id, tabs, selectedTabIdx } = this.props; 16 | const tabElements = tabs.map((tab, i) => 17 | this.onSelectTab(i, tab)} /> 18 | ); 19 | return ( 20 |
21 | {tabElements} 22 |
23 | ); 24 | } 25 | } 26 | 27 | TabBar.propTypes = { 28 | id: PropTypes.string, 29 | onSelectTab: PropTypes.func, 30 | selectedTabIdx: PropTypes.number, 31 | tabs: PropTypes.array, 32 | }; 33 | 34 | export default TabBar; 35 | -------------------------------------------------------------------------------- /webapp/modules/tabview/TabBodies.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import TabBody from "./TabBody"; 5 | 6 | class TabBodies extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | const { tabs } = this.props; 11 | this.tabBodyRefs = tabs.map(() => React.createRef()); 12 | } 13 | 14 | getTab(name) { 15 | const { tabs } = this.props; 16 | const idx = tabs.findIndex((tab) => name === tab.id); 17 | const tab = this.tabBodyRefs[idx].current; 18 | return (idx < 0) ? null : tab; 19 | } 20 | 21 | render() { 22 | const { id, tabs, selectedTabIdx } = this.props; 23 | 24 | const tabBodies = tabs.map((tab, i) => { 25 | const selected = (selectedTabIdx === i); 26 | // Only include the body if it's selected 27 | const clone = React.cloneElement(tab.body, { 28 | ref: this.tabBodyRefs[i], 29 | }); 30 | return {selected ? clone : ""}; 31 | }); 32 | return ( 33 |
34 | {tabBodies} 35 |
36 | ); 37 | } 38 | } 39 | 40 | TabBodies.propTypes = { 41 | id: PropTypes.string, 42 | selectedTabIdx: PropTypes.number, 43 | tabs: PropTypes.array, 44 | }; 45 | 46 | export default TabBodies; 47 | -------------------------------------------------------------------------------- /webapp/modules/tabview/TabBody.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class TabBody extends React.Component { 5 | render() { 6 | const { id, selected } = this.props; 7 | const selectedClass = selected ? "selected" : ""; 8 | return ( 9 |
10 | {this.props.children} 11 |
12 | ); 13 | } 14 | } 15 | 16 | TabBody.propTypes = { 17 | children: PropTypes.node, 18 | id: PropTypes.string, 19 | selected: PropTypes.bool, 20 | }; 21 | 22 | export default TabBody; 23 | -------------------------------------------------------------------------------- /webapp/modules/tabview/TabView.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import TabBar from "./TabBar"; 5 | import TabBodies from "./TabBodies"; 6 | 7 | class TabView extends React.Component { 8 | constructor(...args) { 9 | super(...args); 10 | this.tabBodiesRef = React.createRef(); 11 | this.tableRef = React.createRef(); 12 | } 13 | 14 | onSelectTab = (tabIdx, tab, tabs) => { 15 | const { onSelectedTabChange } = this.props; 16 | onSelectedTabChange(tabIdx, tab, tabs); 17 | } 18 | 19 | getTab(name) { 20 | return this.tabBodiesRef.current.getTab(name); 21 | } 22 | 23 | render() { 24 | const { 25 | id, 26 | tabs, 27 | selectedTabIdx, 28 | showTabBar = true, 29 | } = this.props; 30 | 31 | const hidetabbar = String(showTabBar === false); 32 | 33 | return ( 34 | 35 | 36 | 37 | 43 | 44 | 45 |
38 |
39 | {showTabBar && } 40 | 41 |
42 |
46 | ); 47 | } 48 | } 49 | 50 | TabView.propTypes = { 51 | id: PropTypes.string, 52 | selectedTabIdx: PropTypes.number, 53 | tabs: PropTypes.array, 54 | onSelectedTabChange: PropTypes.func, 55 | showTabBar: PropTypes.bool, 56 | }; 57 | 58 | export default TabView; 59 | -------------------------------------------------------------------------------- /webapp/modules/toolbar/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export const Button = (props) => { 5 | return ( 6 | 11 | {props.children} 12 | 13 | ); 14 | }; 15 | 16 | Button.propTypes = { 17 | buttonRef: PropTypes.string, 18 | children: PropTypes.node, 19 | command: PropTypes.func, 20 | title: PropTypes.string, 21 | }; 22 | 23 | class Toolbar extends React.Component { 24 | createToolbarItems(children) { 25 | const style = { color: "gray" }; 26 | return children.reduce((items, child, i) => { 27 | if (items.length > 0) { 28 | const span = |; 29 | items.push(span); 30 | } 31 | items.push(child); 32 | return items; 33 | }, []); 34 | } 35 | 36 | render() { 37 | let { children } = this.props; 38 | if (!Array.isArray(children)) { 39 | children = [children]; 40 | } 41 | return ( 42 |
43 | {this.createToolbarItems(children || [])} 44 |
45 | ); 46 | } 47 | } 48 | 49 | Toolbar.propTypes = { 50 | children: PropTypes.node, 51 | }; 52 | 53 | export default Toolbar; 54 | -------------------------------------------------------------------------------- /webapp/modules/tree/ObjectTree.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Tree, { createUINode } from "./Tree"; 5 | 6 | export function hasChildren(value) { 7 | if (typeof value === "string") { 8 | // Object.keys(string) returns numeric keys like an array. 9 | // We don't want that. 10 | return false; 11 | } 12 | // arrays have keys, so this works for arrays and objects 13 | return Object.keys(value).length > 0; 14 | } 15 | 16 | export function populateChildren(uiNode, level) { 17 | const object = uiNode.value; 18 | const children = uiNode.children = []; 19 | 20 | const handleChild = (prop, propObj) => { 21 | if (typeof propObj !== "function") { 22 | const child = createUINode("dom", prop, propObj, hasChildren(propObj), level); 23 | children.push(child); 24 | } 25 | }; 26 | 27 | if (Array.isArray(object)) { 28 | object.forEach((item, i) => handleChild(i, item)); 29 | } else { 30 | Object.keys(object).forEach((key) => handleChild(key, object[key])); 31 | } 32 | } 33 | 34 | class ObjectTree extends React.Component { 35 | treeRef = React.createRef(); 36 | 37 | getTree() { 38 | return this.treeRef.current; 39 | } 40 | 41 | render() { 42 | return ; 43 | } 44 | }; 45 | 46 | export default ObjectTree; 47 | -------------------------------------------------------------------------------- /webapp/modules/tree/TreeRepresentations.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import * as Arr from "../core/array"; 4 | 5 | function safeToString(object) { 6 | try { 7 | return object.toString(); 8 | } catch (e) { 9 | return ""; 10 | } 11 | } 12 | 13 | function objectAsStringOrType(object) { 14 | const label = safeToString(object); 15 | const re = /\[object (.*?)\]/; 16 | const m = re.exec(label); 17 | return m ? m[1] : label; 18 | } 19 | 20 | class Representation extends React.Component { 21 | render() { 22 | let { className, value, title } = this.props; 23 | className = className || "object"; 24 | title = title || objectAsStringOrType(value); 25 | return ( 26 |
{title}
27 | ); 28 | } 29 | } 30 | 31 | class NullRepresentation extends React.Component { 32 | render() { 33 | return ; 34 | } 35 | } 36 | 37 | NullRepresentation.supportsObject = function(object, type) { 38 | return object === null; 39 | }; 40 | 41 | class NumberRepresentation extends React.Component { 42 | render() { 43 | const { value } = this.props; 44 | return ; 45 | } 46 | } 47 | 48 | NumberRepresentation.supportsObject = function(object, type) { 49 | return type === "boolean" || type === "number"; 50 | }; 51 | 52 | class StringRepresentation extends React.Component { 53 | render() { 54 | const { value } = this.props; 55 | return ; 56 | } 57 | } 58 | 59 | StringRepresentation.supportsObject = function(object, type) { 60 | return type === "string"; 61 | }; 62 | 63 | class ArrRepresentation extends React.Component { 64 | render() { 65 | const { value } = this.props; 66 | const title = "Array [" + value.length + "]"; 67 | return ; 68 | } 69 | } 70 | 71 | ArrRepresentation.supportsObject = function(object, type) { 72 | return Arr.isArray(object); 73 | }; 74 | 75 | const Representations = { 76 | reps: [ 77 | NullRepresentation, 78 | NumberRepresentation, 79 | StringRepresentation, 80 | ArrRepresentation, 81 | ], 82 | 83 | getRep(object) { 84 | let type = typeof object; 85 | if (type === "object" && object instanceof String) { 86 | type = "string"; 87 | } 88 | const rep = this.reps.find((rep) => rep.supportsObject(object, type)); 89 | if (rep) { 90 | return rep; 91 | } 92 | return Representation; 93 | }, 94 | }; 95 | 96 | export default Representations; 97 | -------------------------------------------------------------------------------- /webapp/modules/tree/XMLTree.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import Tree, { createUINode } from "./Tree"; 5 | 6 | function nodeChildElements(node, limit) { 7 | limit = ("number" === limit) ? limit : null; 8 | 9 | const elements = []; 10 | let child = node.firstChild; 11 | while (child) { 12 | if (Node.ELEMENT_NODE === child.nodeType) { 13 | elements.push(child); 14 | if (null !== limit && elements.length >= limit) { 15 | return elements; 16 | } 17 | } 18 | child = child.nextSibling; 19 | } 20 | return elements; 21 | } 22 | 23 | function nodeAttributes(node, limit) { 24 | limit = ("number" === limit) ? limit : null; 25 | 26 | const attrs = []; 27 | if (!node.attributes) { 28 | return attrs; 29 | } 30 | for (let i = node.attributes.length; --i >= 0;) { 31 | attrs.push(node.attributes[i]); 32 | if (null !== limit && attrs.length >= limit) { 33 | return attrs; 34 | } 35 | } 36 | return attrs; 37 | } 38 | 39 | function hasChildren(value) { 40 | const attrs = nodeAttributes(value); 41 | const elements = nodeChildElements(value); 42 | return (attrs.length > 0 || elements.length > 0); 43 | } 44 | 45 | function populateChildren(uiNode, level) { 46 | const object = uiNode.value; 47 | const children = uiNode.children = []; 48 | 49 | const attrs = nodeAttributes(object) 50 | .map((attr) => createUINode("dom", "@" + attr.name, attr.value, false, level)); 51 | 52 | const elements = nodeChildElements(object).map((element) => { 53 | const hasChilds = hasChildren(element); 54 | return createUINode("dom", element.tagName, hasChilds ? element : element.firstChild.nodeValue, hasChilds, level); 55 | }); 56 | 57 | const members = attrs; 58 | 59 | // If there are no child elements, then we add the element's firstChild content (if it exists). 60 | // This firstChild content could be text. 61 | if (elements.length === 0 && object.firstChild) { 62 | members.push(createUINode("dom", "value", object.firstChild.nodeValue, false, level)); 63 | } 64 | 65 | children.push(...attrs); 66 | children.push(...elements); 67 | } 68 | 69 | class XMLTree extends React.Component { 70 | render() { 71 | const { root } = this.props; 72 | return ; 73 | } 74 | }; 75 | 76 | XMLTree.propTypes = { 77 | root: PropTypes.object, 78 | }; 79 | 80 | XMLTree.isXmlMimeType = function(mimeType) { 81 | mimeType = Mime.extractMimeType(mimeType); 82 | return [ 83 | "text/xml", 84 | "application/xml", 85 | "image/svg+xml", 86 | "application/atom+xml", 87 | "application/xslt+xml", 88 | "application/mathml+xml", 89 | "application/rss+xml", 90 | ].indexOf(mimeType) > -1; 91 | }; 92 | 93 | XMLTree.canShowEntry = function(entry) { 94 | const { content } = entry.response; 95 | 96 | if (!content || !content.text) { 97 | return false; 98 | } 99 | 100 | if (!canDecode(content.encoding)) { 101 | return false; 102 | } 103 | 104 | const mimeType = Mime.extractMimeType(content.mimeType || ""); 105 | return XMLTree.isXmlMimeType(mimeType); 106 | }; 107 | 108 | export default XMLTree; 109 | -------------------------------------------------------------------------------- /webapp/preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HTTP Archive Preview @VERSION@ 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 31 | 32 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const packageJson = require("./package.json"); 4 | const GitRevisionPlugin = require("git-revision-webpack-plugin"); 5 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; 6 | 7 | // Export some data to the webpack build. See buildInfo.js. 8 | const gitRevisionPlugin = new GitRevisionPlugin(); 9 | const definePlugin = new webpack.DefinePlugin({ 10 | __buildInfo__: { 11 | version: JSON.stringify(packageJson.version), 12 | gitVersion: JSON.stringify(gitRevisionPlugin.version()), 13 | gitCommit: JSON.stringify(gitRevisionPlugin.commithash()), 14 | }, 15 | }); 16 | 17 | function makeConfig({ 18 | target = "", 19 | babelPresetEnvUseBuiltIns = false, 20 | }) { 21 | const babelPresetEnvTargets = (target === "legacy") 22 | ? { 23 | "ie": "11", 24 | } 25 | : [ 26 | "last 2 versions", 27 | "not ie < 999", 28 | "not android < 999", 29 | "not dead", 30 | ]; 31 | 32 | return { 33 | mode: "development", 34 | entry: { 35 | // polyfill required by (at least) IE11 36 | main: [ 37 | "whatwg-fetch", 38 | "./webapp/main.js", 39 | ], 40 | demo: [ 41 | "whatwg-fetch", 42 | "./webapp/demo.js", 43 | ], 44 | }, 45 | output: { 46 | path: path.join(__dirname, "webapp", "dist"), 47 | publicPath: "/webapp/dist/", 48 | filename: `[name].${target ? target + "." : ""}js`, 49 | }, 50 | devtool: "source-map", 51 | plugins: [ 52 | definePlugin, 53 | new BundleAnalyzerPlugin({ 54 | openAnalyzer: false, 55 | generateStatsFile: true, 56 | statsFilename: path.join(__dirname, "reports", (target ? target + "." : "") + "stats.json"), 57 | analyzerMode: "static", 58 | reportFilename: path.join(__dirname, "reports", (target ? target + "." : "") + "report.html"), 59 | }), 60 | ], 61 | optimization: { 62 | splitChunks: { 63 | cacheGroups: { 64 | commons: { 65 | test: /[\\/]node_modules[\\/]/, 66 | name: "vendor", 67 | chunks: "initial", 68 | }, 69 | }, 70 | }, 71 | }, 72 | module: { 73 | rules: [ 74 | { 75 | // .js or .jsx 76 | test: /\.jsx?$/, 77 | use: { 78 | loader: "babel-loader", 79 | options: { 80 | presets: [ 81 | ["@babel/preset-env", { 82 | useBuiltIns: babelPresetEnvUseBuiltIns, 83 | targets: babelPresetEnvTargets, 84 | debug: true, 85 | }], 86 | "@babel/preset-react", 87 | ], 88 | // transform-runtime required for things like Object.assign() for browsers that don't support that. 89 | // rest-spread transform required for "valuelink"" module 90 | plugins: [ 91 | "transform-class-properties", 92 | ], 93 | }, 94 | }, 95 | }, 96 | { 97 | test: /\.css$/, 98 | use: [ 99 | { 100 | loader: "style-loader", 101 | }, 102 | { 103 | loader: "css-loader", 104 | }, 105 | ], 106 | }, 107 | { 108 | // inline base64 URLs for <=8k images, direct URLs for the rest 109 | test: /\.(png|jpg)$/, 110 | loader: "url-loader?limit=8192", 111 | }, 112 | ], 113 | }, 114 | devServer: { 115 | // Until all the test resources are part of the harviewer-react project, 116 | // use the original harviewer resources via proxy. 117 | proxy: { 118 | "/selenium": "http://harviewer.lan:49001", 119 | }, 120 | }, 121 | resolveLoader: { 122 | alias: { 123 | "i18n": "amdi18n-loader", 124 | }, 125 | }, 126 | }; 127 | } 128 | 129 | module.exports = (env, options) => { 130 | const prod = options && options.mode === "production"; 131 | const babelPresetEnvUseBuiltIns = prod ? "entry" : false; 132 | return [ 133 | makeConfig({ 134 | babelPresetEnvUseBuiltIns, 135 | }), 136 | makeConfig({ 137 | target: "legacy", 138 | babelPresetEnvUseBuiltIns, 139 | }), 140 | ]; 141 | }; 142 | --------------------------------------------------------------------------------