From 2e05feac2534839700aed4112755dbd4e6c99cc5 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 7 Jan 2021 16:15:34 -0800 Subject: [PATCH] feat(cli): bring in trace viewer (#4920) --- .eslintrc.js | 3 + package-lock.json | 807 ++++++++++++++++++ package.json | 10 + src/cli/cli.ts | 17 + src/cli/injected/recorder.webpack.config.js | 1 + src/cli/traceViewer/screenshotGenerator.ts | 136 +++ src/cli/traceViewer/snapshotRouter.ts | 122 +++ src/cli/traceViewer/traceModel.ts | 154 ++++ src/cli/traceViewer/traceViewer.ts | 181 ++++ src/cli/traceViewer/videoTileGenerator.ts | 87 ++ src/cli/traceViewer/web/common.css | 122 +++ src/cli/traceViewer/web/geometry.ts | 25 + src/cli/traceViewer/web/index.html | 27 + src/cli/traceViewer/web/index.tsx | 57 ++ .../web/third_party/vscode/LICENSE.txt | 21 + .../web/third_party/vscode/codicon.css | 440 ++++++++++ .../web/third_party/vscode/codicon.ttf | Bin 0 -> 61024 bytes src/cli/traceViewer/web/ui/actionList.css | 92 ++ src/cli/traceViewer/web/ui/actionList.tsx | 42 + .../traceViewer/web/ui/contextSelector.css | 30 + .../traceViewer/web/ui/contextSelector.tsx | 41 + src/cli/traceViewer/web/ui/filmStrip.css | 45 + src/cli/traceViewer/web/ui/filmStrip.tsx | 136 +++ src/cli/traceViewer/web/ui/helpers.tsx | 73 ++ src/cli/traceViewer/web/ui/networkTab.css | 68 ++ src/cli/traceViewer/web/ui/networkTab.tsx | 39 + .../web/ui/propertiesTabbedPane.css | 86 ++ .../web/ui/propertiesTabbedPane.tsx | 88 ++ src/cli/traceViewer/web/ui/sourceTab.css | 43 + src/cli/traceViewer/web/ui/sourceTab.tsx | 75 ++ src/cli/traceViewer/web/ui/timeline.css | 110 +++ src/cli/traceViewer/web/ui/timeline.tsx | 156 ++++ src/cli/traceViewer/web/ui/workbench.css | 59 ++ src/cli/traceViewer/web/ui/workbench.tsx | 67 ++ src/cli/traceViewer/web/web.webpack.config.js | 40 + .../injected/consoleApi.webpack.config.js | 1 + .../injected/injectedScript.webpack.config.js | 1 + .../injected/utilityScript.webpack.config.js | 1 + .../highlightjs/highlightjs/tomorrow.css | 72 ++ src/third_party/highlightjs/roll.sh | 2 + tsconfig.json | 5 +- utils/build/build.js | 3 +- utils/check_deps.js | 2 +- 43 files changed, 3583 insertions(+), 4 deletions(-) create mode 100644 src/cli/traceViewer/screenshotGenerator.ts create mode 100644 src/cli/traceViewer/snapshotRouter.ts create mode 100644 src/cli/traceViewer/traceModel.ts create mode 100644 src/cli/traceViewer/traceViewer.ts create mode 100644 src/cli/traceViewer/videoTileGenerator.ts create mode 100644 src/cli/traceViewer/web/common.css create mode 100644 src/cli/traceViewer/web/geometry.ts create mode 100644 src/cli/traceViewer/web/index.html create mode 100644 src/cli/traceViewer/web/index.tsx create mode 100644 src/cli/traceViewer/web/third_party/vscode/LICENSE.txt create mode 100644 src/cli/traceViewer/web/third_party/vscode/codicon.css create mode 100644 src/cli/traceViewer/web/third_party/vscode/codicon.ttf create mode 100644 src/cli/traceViewer/web/ui/actionList.css create mode 100644 src/cli/traceViewer/web/ui/actionList.tsx create mode 100644 src/cli/traceViewer/web/ui/contextSelector.css create mode 100644 src/cli/traceViewer/web/ui/contextSelector.tsx create mode 100644 src/cli/traceViewer/web/ui/filmStrip.css create mode 100644 src/cli/traceViewer/web/ui/filmStrip.tsx create mode 100644 src/cli/traceViewer/web/ui/helpers.tsx create mode 100644 src/cli/traceViewer/web/ui/networkTab.css create mode 100644 src/cli/traceViewer/web/ui/networkTab.tsx create mode 100644 src/cli/traceViewer/web/ui/propertiesTabbedPane.css create mode 100644 src/cli/traceViewer/web/ui/propertiesTabbedPane.tsx create mode 100644 src/cli/traceViewer/web/ui/sourceTab.css create mode 100644 src/cli/traceViewer/web/ui/sourceTab.tsx create mode 100644 src/cli/traceViewer/web/ui/timeline.css create mode 100644 src/cli/traceViewer/web/ui/timeline.tsx create mode 100644 src/cli/traceViewer/web/ui/workbench.css create mode 100644 src/cli/traceViewer/web/ui/workbench.tsx create mode 100644 src/cli/traceViewer/web/web.webpack.config.js create mode 100644 src/third_party/highlightjs/highlightjs/tomorrow.css diff --git a/.eslintrc.js b/.eslintrc.js index 5e7b9c9da5..e7989b5893 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,6 +5,9 @@ module.exports = { ecmaVersion: 9, sourceType: 'module', }, + extends: [ + 'plugin:react-hooks/recommended' + ], /** * ESLint rules diff --git a/package-lock.json b/package-lock.json index b8ca89c893..05d41c2665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1161,6 +1161,12 @@ "defer-to-connect": "^1.0.1" } }, + "@types/anymatch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", + "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", + "dev": true + }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -1189,6 +1195,12 @@ "@types/node": "*" } }, + "@types/html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", @@ -1255,6 +1267,12 @@ "@types/node": "*" } }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, "@types/proper-lockfile": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.1.tgz", @@ -1273,6 +1291,31 @@ "@types/node": "*" } }, + "@types/react": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", + "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", + "integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/resize-observer-browser": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.4.tgz", + "integrity": "sha512-rPvqs+1hL/5hbES/0HTdUu4lvNmneiwKwccbWe7HGLWbnsLdqKnQHyWLg4Pj0AMO7PLHCwBM1Cs8orChdkDONg==", + "dev": true + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -1289,12 +1332,82 @@ "@types/node": "*" } }, + "@types/source-list-map": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", + "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/tapable": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz", + "integrity": "sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==", + "dev": true + }, + "@types/uglify-js": { + "version": "3.11.1", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.1.tgz", + "integrity": "sha512-7npvPKV+jINLu1SpSYVWG8KvyJBhBa8tmzMMdDoVc2pWUYHN8KIXlPJhjJ4LT97c4dXJA2SHL/q6ADbDriZN+Q==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack": { + "version": "4.41.25", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.25.tgz", + "integrity": "sha512-cr6kZ+4m9lp86ytQc1jPOJXgINQyz3kLLunZ57jznW+WIAL0JqZbGubQk4GlD42MuQL5JGOABrxdpqqWeovlVQ==", + "dev": true, + "requires": { + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@types/webpack-sources": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz", + "integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, "@types/ws": { "version": "7.2.6", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz", @@ -1903,6 +2016,12 @@ "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", "dev": true }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, "boolean": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.1.tgz", @@ -2154,12 +2273,40 @@ } } }, + "call-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", + "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "requires": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + } + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -2287,6 +2434,23 @@ } } }, + "clean-css": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", + "dev": true, + "requires": { + "source-map": "~0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "cli": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/cli/-/cli-0.4.5.tgz", @@ -2607,6 +2771,86 @@ "randomfill": "^1.0.3" } }, + "css-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-4.3.0.tgz", + "integrity": "sha512-rdezjCjScIrsL8BSYszgT4s476IcNKt6yX69t0pHjJVnPUTDpn4WfIpDQTN3wCJvUvfsz/mFjuGOekf3PY3NUg==", + "dev": true, + "requires": { + "camelcase": "^6.0.0", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^2.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.3", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.1", + "semver": "^7.3.2" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "csstype": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", + "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==", + "dev": true + }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -2767,12 +3011,82 @@ "esutils": "^2.0.2" } }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + }, + "dependencies": { + "domelementtype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", + "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==", + "dev": true + } + } + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + } + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -3049,6 +3363,12 @@ "metric-lcs": "^0.1.2" } }, + "eslint-plugin-react-hooks": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", + "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -3396,6 +3716,40 @@ "flat-cache": "^2.0.1" } }, + "file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -3744,6 +4098,17 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", + "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -4039,6 +4404,12 @@ "minimalistic-assert": "^1.0.1" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -4059,6 +4430,79 @@ "parse-passwd": "^1.0.0" } }, + "html-minifier-terser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz", + "integrity": "sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg==", + "dev": true, + "requires": { + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" + }, + "dependencies": { + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + } + } + }, + "html-webpack-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.1.tgz", + "integrity": "sha512-yzK7RQZwv9xB+pcdHNTjcqbaaDZ+5L0zJHXfi89iWIZmb/FtzxhLk0635rmJihcQbs3ZUF27Xp4oWGx6EK56zg==", + "dev": true, + "requires": { + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.20", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", + "util.promisify": "1.0.0" + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -4080,6 +4524,15 @@ "debug": "4" } }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -4124,6 +4577,12 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -4550,6 +5009,32 @@ "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + } + } + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -4840,6 +5325,24 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + } + } + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", @@ -4920,6 +5423,15 @@ "pify": "^3.0.0" } }, + "nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "requires": { + "boolbase": "~1.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4990,6 +5502,17 @@ "object-keys": "^1.1.1" } }, + "object.getownpropertydescriptors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -5074,6 +5597,24 @@ "readable-stream": "^2.1.5" } }, + "param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "requires": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + } + } + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5102,6 +5643,24 @@ "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", + "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==", + "dev": true + } + } + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -5227,6 +5786,121 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", + "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5239,6 +5913,16 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "dev": true }, + "pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "dev": true, + "requires": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, "pretty-format": { "version": "26.6.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.1.tgz", @@ -5419,6 +6103,27 @@ "safe-buffer": "^5.1.0" } }, + "react": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", + "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", + "integrity": "sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.1" + } + }, "react-is": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", @@ -5532,6 +6237,12 @@ } } }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "dev": true + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -5539,6 +6250,36 @@ "dev": true, "optional": true }, + "renderkid": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", + "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "dev": true, + "requires": { + "css-select": "^2.0.2", + "dom-converter": "^0.2", + "htmlparser2": "^3.10.1", + "lodash": "^4.17.20", + "strip-ansi": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "repeat-element": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", @@ -5717,6 +6458,16 @@ "truncate-utf8-bytes": "^1.0.0" } }, + "scheduler": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.1.tgz", + "integrity": "sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==", + "dev": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -6227,6 +6978,40 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-loader": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", + "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + } + } + } + }, "sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -6540,6 +7325,12 @@ "set-value": "^2.0.1" } }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, "unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -6688,6 +7479,22 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, + "util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", + "dev": true + }, "v8-compile-cache": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", diff --git a/package.json b/package.json index af55cc53e0..73cb97e5b5 100644 --- a/package.json +++ b/package.json @@ -58,22 +58,32 @@ "@types/progress": "^2.0.3", "@types/proper-lockfile": "^4.1.1", "@types/proxy-from-env": "^1.0.1", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "@types/resize-observer-browser": "^0.1.4", "@types/rimraf": "^3.0.0", "@types/ws": "7.2.6", "@typescript-eslint/eslint-plugin": "^3.10.1", "@typescript-eslint/parser": "^3.10.1", "chokidar": "^3.5.0", + "css-loader": "^4.3.0", "colors": "^1.4.0", "commonmark": "^0.29.1", "cross-env": "^7.0.2", "electron": "^9.2.1", "eslint": "^7.7.0", "eslint-plugin-notice": "^0.9.10", + "eslint-plugin-react-hooks": "^4.2.0", + "file-loader": "^6.1.0", "folio": "=0.3.16", "formidable": "^1.2.2", + "html-webpack-plugin": "^4.4.1", "ncp": "^2.0.0", "node-stream-zip": "^1.11.3", + "react": "^17.0.1", + "react-dom": "^17.0.1", "socksv5": "0.0.6", + "style-loader": "^1.2.1", "ts-loader": "^8.0.3", "typescript": "^4.0.2", "webpack": "^4.44.2", diff --git a/src/cli/cli.ts b/src/cli/cli.ts index b84e9a7433..8344a21976 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -30,6 +30,7 @@ import { PythonLanguageGenerator } from './codegen/languages/python'; import { CSharpLanguageGenerator } from './codegen/languages/csharp'; import { RecorderController } from './codegen/recorderController'; import { runServer, printApiJson, installBrowsers } from './driver'; +import { showTraceViewer } from './traceViewer/traceViewer'; import type { Browser, BrowserContext, Page, BrowserType, BrowserContextOptions, LaunchOptions } from '../..'; import * as playwright from '../..'; @@ -136,6 +137,22 @@ program }); }); +if (process.env.PWTRACE) { + program + .command('show-trace ') + .description('Show trace viewer') + .option('--resources ', 'Directory with the shared trace artifacts') + .action(function(trace, command) { + showTraceViewer(command.resources, trace); + }).on('--help', function() { + console.log(''); + console.log('Examples:'); + console.log(''); + console.log(' $ show-trace --resources=resources trace/file.trace'); + console.log(' $ show-trace trace/directory'); + }); +} + if (process.argv[2] === 'run-driver') runServer(); else if (process.argv[2] === 'print-api-json') diff --git a/src/cli/injected/recorder.webpack.config.js b/src/cli/injected/recorder.webpack.config.js index ba86b05300..237bbfaaa3 100644 --- a/src/cli/injected/recorder.webpack.config.js +++ b/src/cli/injected/recorder.webpack.config.js @@ -20,6 +20,7 @@ const InlineSource = require('../../server/injected/webpack-inline-source-plugin module.exports = { entry: path.join(__dirname, 'recorder.ts'), devtool: 'source-map', + mode: 'development', module: { rules: [ { diff --git a/src/cli/traceViewer/screenshotGenerator.ts b/src/cli/traceViewer/screenshotGenerator.ts new file mode 100644 index 0000000000..ebe4d12bb2 --- /dev/null +++ b/src/cli/traceViewer/screenshotGenerator.ts @@ -0,0 +1,136 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as playwright from '../../..'; +import * as util from 'util'; +import { SnapshotRouter } from './snapshotRouter'; +import { actionById, ActionEntry, ContextEntry, TraceModel } from './traceModel'; +import type { PageSnapshot } from '../../trace/traceTypes'; + +const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); +const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); + +export class ScreenshotGenerator { + private _traceStorageDir: string; + private _browserPromise: Promise | undefined; + private _traceModel: TraceModel; + private _rendering = new Map>(); + + constructor(traceStorageDir: string, traceModel: TraceModel) { + this._traceStorageDir = traceStorageDir; + this._traceModel = traceModel; + } + + async generateScreenshot(actionId: string): Promise { + const { context, action } = actionById(this._traceModel, actionId); + if (!action.action.snapshot) + return; + const imageFileName = path.join(this._traceStorageDir, action.action.snapshot.sha1 + '-thumbnail.png'); + + let body: Buffer | undefined; + try { + body = await fsReadFileAsync(imageFileName); + } catch (e) { + if (!this._rendering.has(action)) { + this._rendering.set(action, this._render(context, action, imageFileName).then(body => { + this._rendering.delete(action); + return body; + })); + } + body = await this._rendering.get(action)!; + } + return body; + } + + private _browser() { + if (!this._browserPromise) + this._browserPromise = playwright.chromium.launch(); + return this._browserPromise; + } + + private async _render(contextEntry: ContextEntry, actionEntry: ActionEntry, imageFileName: string): Promise { + const { action } = actionEntry; + const browser = await this._browser(); + const page = await browser.newPage({ + viewport: contextEntry.created.viewportSize, + deviceScaleFactor: contextEntry.created.deviceScaleFactor + }); + + try { + const snapshotPath = path.join(this._traceStorageDir, action.snapshot!.sha1); + let snapshot; + try { + snapshot = await fsReadFileAsync(snapshotPath, 'utf8'); + } catch (e) { + console.log(`Unable to read snapshot at ${snapshotPath}`); // eslint-disable-line no-console + return; + } + const snapshotObject = JSON.parse(snapshot) as PageSnapshot; + const snapshotRouter = new SnapshotRouter(this._traceStorageDir); + snapshotRouter.selectSnapshot(snapshotObject, contextEntry); + page.route('**/*', route => snapshotRouter.route(route)); + const url = snapshotObject.frames[0].url; + console.log('Generating screenshot for ' + action.action, snapshotObject.frames[0].url); // eslint-disable-line no-console + await page.goto(url); + + let clip: any = undefined; + const element = await page.$(action.selector || '*[__playwright_target__]'); + if (element) { + await element.evaluate(e => { + e.style.backgroundColor = '#ff69b460'; + }); + + clip = await element.boundingBox() || undefined; + if (clip) { + const thumbnailSize = { + width: 400, + height: 200 + }; + const insets = { + width: 60, + height: 30 + }; + clip.width = Math.min(thumbnailSize.width, clip.width); + clip.height = Math.min(thumbnailSize.height, clip.height); + if (clip.width < thumbnailSize.width) { + clip.x -= (thumbnailSize.width - clip.width) / 2; + clip.x = Math.max(0, clip.x); + clip.width = thumbnailSize.width; + } else { + clip.x = Math.max(0, clip.x - insets.width); + } + if (clip.height < thumbnailSize.height) { + clip.y -= (thumbnailSize.height - clip.height) / 2; + clip.y = Math.max(0, clip.y); + clip.height = thumbnailSize.height; + } else { + clip.y = Math.max(0, clip.y - insets.height); + } + } + } + + const imageData = await page.screenshot({ clip }); + await fsWriteFileAsync(imageFileName, imageData); + return imageData; + } catch (e) { + console.log(e); // eslint-disable-line no-console + } finally { + await page.close(); + } + } +} diff --git a/src/cli/traceViewer/snapshotRouter.ts b/src/cli/traceViewer/snapshotRouter.ts new file mode 100644 index 0000000000..6cf781826a --- /dev/null +++ b/src/cli/traceViewer/snapshotRouter.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; +import type { Route } from '../../..'; +import type { FrameSnapshot, NetworkResourceTraceEvent, PageSnapshot } from '../../trace/traceTypes'; +import { ContextEntry } from './traceModel'; + +const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); + +export class SnapshotRouter { + private _contextEntry: ContextEntry | undefined; + private _unknownUrls = new Set(); + private _traceStorageDir: string; + private _frameBySrc = new Map(); + + constructor(traceStorageDir: string) { + this._traceStorageDir = traceStorageDir; + } + + selectSnapshot(snapshot: PageSnapshot, contextEntry: ContextEntry) { + this._frameBySrc.clear(); + this._contextEntry = contextEntry; + for (const frameSnapshot of snapshot.frames) + this._frameBySrc.set(frameSnapshot.url, frameSnapshot); + } + + async route(route: Route) { + const url = route.request().url(); + if (this._frameBySrc.has(url)) { + const frameSnapshot = this._frameBySrc.get(url)!; + route.fulfill({ + contentType: 'text/html', + body: Buffer.from(frameSnapshot.html), + }); + return; + } + + const frameSrc = route.request().frame().url(); + const frameSnapshot = this._frameBySrc.get(frameSrc); + if (!frameSnapshot) + return this._routeUnknown(route); + + // Find a matching resource from the same context, preferrably from the same frame. + // Note: resources are stored without hash, but page may reference them with hash. + let resource: NetworkResourceTraceEvent | null = null; + const resourcesWithUrl = this._contextEntry!.resourcesByUrl.get(removeHash(url)) || []; + for (const resourceEvent of resourcesWithUrl) { + if (resource && resourceEvent.frameId !== frameSnapshot.frameId) + continue; + resource = resourceEvent; + if (resourceEvent.frameId === frameSnapshot.frameId) + break; + } + if (!resource) + return this._routeUnknown(route); + + // This particular frame might have a resource content override, for example when + // stylesheet is modified using CSSOM. + const resourceOverride = frameSnapshot.resourceOverrides.find(o => o.url === url); + const overrideSha1 = resourceOverride ? resourceOverride.sha1 : undefined; + const resourceData = await this._readResource(resource, overrideSha1); + if (!resourceData) + return this._routeUnknown(route); + const headers: { [key: string]: string } = {}; + for (const { name, value } of resourceData.headers) + headers[name] = value; + headers['Access-Control-Allow-Origin'] = '*'; + route.fulfill({ + contentType: resourceData.contentType, + body: resourceData.body, + headers, + }); + } + + private _routeUnknown(route: Route) { + const url = route.request().url(); + if (!this._unknownUrls.has(url)) { + console.log(`Request to unknown url: ${url}`); /* eslint-disable-line no-console */ + this._unknownUrls.add(url); + } + route.abort(); + } + + private async _readResource(event: NetworkResourceTraceEvent, overrideSha1: string | undefined) { + try { + const body = await fsReadFileAsync(path.join(this._traceStorageDir, overrideSha1 || event.sha1)); + return { + contentType: event.contentType, + body, + headers: event.responseHeaders, + }; + } catch (e) { + return undefined; + } + } +} + +function removeHash(url: string) { + try { + const u = new URL(url); + u.hash = ''; + return u.toString(); + } catch (e) { + return url; + } +} diff --git a/src/cli/traceViewer/traceModel.ts b/src/cli/traceViewer/traceModel.ts new file mode 100644 index 0000000000..2051ebce9c --- /dev/null +++ b/src/cli/traceViewer/traceModel.ts @@ -0,0 +1,154 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as trace from '../../trace/traceTypes'; + +export type TraceModel = { + contexts: ContextEntry[]; +} + +export type ContextEntry = { + name: string; + filePath: string; + startTime: number; + endTime: number; + created: trace.ContextCreatedTraceEvent; + destroyed: trace.ContextDestroyedTraceEvent; + pages: PageEntry[]; + resourcesByUrl: Map; +} + +export type VideoEntry = { + video: trace.PageVideoTraceEvent; + videoId: string; +}; + +export type PageEntry = { + created: trace.PageCreatedTraceEvent; + destroyed: trace.PageDestroyedTraceEvent; + video?: VideoEntry; + actions: ActionEntry[]; + resources: trace.NetworkResourceTraceEvent[]; +} + +export type ActionEntry = { + actionId: string; + action: trace.ActionTraceEvent; + resources: trace.NetworkResourceTraceEvent[]; +}; + +export type VideoMetaInfo = { + frames: number; + width: number; + height: number; + fps: number; + startTime: number; + endTime: number; +}; + +export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel, filePath: string) { + const contextEntries = new Map(); + const pageEntries = new Map(); + + for (const event of events) { + switch (event.type) { + case 'context-created': { + contextEntries.set(event.contextId, { + filePath, + name: filePath.substring(filePath.lastIndexOf('/') + 1), + startTime: Number.MAX_VALUE, + endTime: Number.MIN_VALUE, + created: event, + destroyed: undefined as any, + pages: [], + resourcesByUrl: new Map(), + }); + break; + } + case 'context-destroyed': { + contextEntries.get(event.contextId)!.destroyed = event; + break; + } + case 'page-created': { + const pageEntry: PageEntry = { + created: event, + destroyed: undefined as any, + actions: [], + resources: [], + }; + pageEntries.set(event.pageId, pageEntry); + contextEntries.get(event.contextId)!.pages.push(pageEntry); + break; + } + case 'page-destroyed': { + pageEntries.get(event.pageId)!.destroyed = event; + break; + } + case 'page-video': { + const pageEntry = pageEntries.get(event.pageId)!; + pageEntry.video = { video: event, videoId: event.contextId + '/' + event.pageId }; + break; + } + case 'action': { + const pageEntry = pageEntries.get(event.pageId!)!; + const action: ActionEntry = { + actionId: event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length, + action: event, + resources: pageEntry.resources, + }; + pageEntry.resources = []; + pageEntry.actions.push(action); + break; + } + case 'resource': { + const contextEntry = contextEntries.get(event.contextId)!; + const pageEntry = pageEntries.get(event.pageId!)!; + const action = pageEntry.actions[pageEntry.actions.length - 1]; + if (action) + action.resources.push(event); + else + pageEntry.resources.push(event); + let responseEvents = contextEntry.resourcesByUrl.get(event.url); + if (!responseEvents) { + responseEvents = []; + contextEntry.resourcesByUrl.set(event.url, responseEvents); + } + responseEvents.push(event); + break; + } + } + + const contextEntry = contextEntries.get(event.contextId)!; + contextEntry.startTime = Math.min(contextEntry.startTime, (event as any).timestamp); + contextEntry.endTime = Math.max(contextEntry.endTime, (event as any).timestamp); + } + traceModel.contexts.push(...contextEntries.values()); +} + +export function actionById(traceModel: TraceModel, actionId: string): { context: ContextEntry, page: PageEntry, action: ActionEntry } { + const [contextId, pageId, actionIndex] = actionId.split('/'); + const context = traceModel.contexts.find(entry => entry.created.contextId === contextId)!; + const page = context.pages.find(entry => entry.created.pageId === pageId)!; + const action = page.actions[+actionIndex]; + return { context, page, action }; +} + +export function videoById(traceModel: TraceModel, videoId: string): { context: ContextEntry, page: PageEntry } { + const [contextId, pageId] = videoId.split('/'); + const context = traceModel.contexts.find(entry => entry.created.contextId === contextId)!; + const page = context.pages.find(entry => entry.created.pageId === pageId)!; + return { context, page }; +} diff --git a/src/cli/traceViewer/traceViewer.ts b/src/cli/traceViewer/traceViewer.ts new file mode 100644 index 0000000000..279d453842 --- /dev/null +++ b/src/cli/traceViewer/traceViewer.ts @@ -0,0 +1,181 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as playwright from '../../..'; +import * as util from 'util'; +import { ScreenshotGenerator } from './screenshotGenerator'; +import { SnapshotRouter } from './snapshotRouter'; +import { readTraceFile, TraceModel } from './traceModel'; +import type { ActionTraceEvent, PageSnapshot, TraceEvent } from '../../trace/traceTypes'; +import { VideoTileGenerator } from './videoTileGenerator'; + +const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); + +class TraceViewer { + private _traceStorageDir: string; + private _traceModel: TraceModel; + private _snapshotRouter: SnapshotRouter; + private _screenshotGenerator: ScreenshotGenerator; + private _videoTileGenerator: VideoTileGenerator; + + constructor(traceStorageDir: string) { + this._traceStorageDir = traceStorageDir; + this._snapshotRouter = new SnapshotRouter(traceStorageDir); + this._traceModel = { + contexts: [], + }; + this._screenshotGenerator = new ScreenshotGenerator(traceStorageDir, this._traceModel); + this._videoTileGenerator = new VideoTileGenerator(this._traceModel); + } + + async load(filePath: string) { + const traceContent = await fsReadFileAsync(filePath, 'utf8'); + const events = traceContent.split('\n').map(line => line.trim()).filter(line => !!line).map(line => JSON.parse(line)) as TraceEvent[]; + readTraceFile(events, this._traceModel, filePath); + } + + async show() { + const browser = await playwright.chromium.launch({ headless: false }); + const uiPage = await browser.newPage({ viewport: null }); + uiPage.on('close', () => process.exit(0)); + await uiPage.exposeBinding('readFile', async (_, path: string) => { + return fs.readFileSync(path).toString(); + }); + await uiPage.exposeBinding('renderSnapshot', async (_, action: ActionTraceEvent) => { + try { + if (!action.snapshot) { + const snapshotFrame = uiPage.frames()[1]; + await snapshotFrame.goto('data:text/html,No snapshot available'); + return; + } + + const snapshot = await fsReadFileAsync(path.join(this._traceStorageDir, action.snapshot!.sha1), 'utf8'); + const snapshotObject = JSON.parse(snapshot) as PageSnapshot; + const contextEntry = this._traceModel.contexts.find(entry => entry.created.contextId === action.contextId)!; + this._snapshotRouter.selectSnapshot(snapshotObject, contextEntry); + + // TODO: fix Playwright bug where frame.name is lost (empty). + const snapshotFrame = uiPage.frames()[1]; + try { + await snapshotFrame.goto(snapshotObject.frames[0].url); + } catch (e) { + if (!e.message.includes('frame was detached')) + console.error(e); + return; + } + const element = await snapshotFrame.$(action.selector || '*[__playwright_target__]'); + if (element) { + await element.evaluate(e => { + e.style.backgroundColor = '#ff69b460'; + }); + } + } catch (e) { + console.log(e); // eslint-disable-line no-console + } + }); + await uiPage.exposeBinding('getTraceModel', () => this._traceModel); + await uiPage.exposeBinding('getVideoMetaInfo', async (_, videoId: string) => { + return this._videoTileGenerator.render(videoId); + }); + await uiPage.route('**/*', (route, request) => { + if (request.frame().parentFrame()) { + this._snapshotRouter.route(route); + return; + } + const url = new URL(request.url()); + try { + if (request.url().includes('action-preview')) { + const fullPath = url.pathname.substring('/action-preview/'.length); + const actionId = fullPath.substring(0, fullPath.indexOf('.png')); + this._screenshotGenerator.generateScreenshot(actionId).then(body => { + if (body) + route.fulfill({ contentType: 'image/png', body }); + else + route.fulfill({ status: 404 }); + }); + return; + } + let filePath: string; + if (request.url().includes('video-tile')) { + const fullPath = url.pathname.substring('/video-tile/'.length); + filePath = this._videoTileGenerator.tilePath(fullPath); + } else { + filePath = path.join(__dirname, 'web', url.pathname.substring(1)); + } + const body = fs.readFileSync(filePath); + route.fulfill({ + contentType: extensionToMime[path.extname(url.pathname).substring(1)] || 'text/plain', + body, + }); + } catch (e) { + console.log(e); // eslint-disable-line no-console + route.fulfill({ + status: 404 + }); + } + }); + await uiPage.goto('http://trace-viewer/index.html'); + } +} + +export async function showTraceViewer(traceStorageDir: string | undefined, tracePath: string) { + if (!fs.existsSync(tracePath)) + throw new Error(`${tracePath} does not exist`); + + let files: string[]; + if (fs.statSync(tracePath).isFile()) { + files = [tracePath]; + if (!traceStorageDir) + traceStorageDir = path.dirname(tracePath); + } else { + files = collectFiles(tracePath); + if (!traceStorageDir) + traceStorageDir = tracePath; + } + + const traceViewer = new TraceViewer(traceStorageDir); + for (const filePath of files) + await traceViewer.load(filePath); + await traceViewer.show(); +} + +function collectFiles(dir: string): string[] { + const files = []; + for (const name of fs.readdirSync(dir)) { + const fullName = path.join(dir, name); + if (fs.lstatSync(fullName).isDirectory()) + files.push(...collectFiles(fullName)); + else if (fullName.endsWith('.trace')) + files.push(fullName); + } + return files; +} + +const extensionToMime: { [key: string]: string } = { + 'css': 'text/css', + 'html': 'text/html', + 'jpeg': 'image/jpeg', + 'jpg': 'image/jpeg', + 'js': 'application/javascript', + 'png': 'image/png', + 'ttf': 'font/ttf', + 'svg': 'image/svg+xml', + 'webp': 'image/webp', + 'woff': 'font/woff', + 'woff2': 'font/woff2', +}; diff --git a/src/cli/traceViewer/videoTileGenerator.ts b/src/cli/traceViewer/videoTileGenerator.ts new file mode 100644 index 0000000000..146c048712 --- /dev/null +++ b/src/cli/traceViewer/videoTileGenerator.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as util from 'util'; +import { TraceModel, videoById, VideoMetaInfo } from './traceModel'; +import type { PageVideoTraceEvent } from '../../trace/traceTypes'; +import { ffmpegExecutable } from '../../utils/binaryPaths'; + +const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); +const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); + +export class VideoTileGenerator { + private _traceModel: TraceModel; + + constructor(traceModel: TraceModel) { + this._traceModel = traceModel; + } + + tilePath(urlPath: string) { + const index = urlPath.lastIndexOf('/'); + const tile = urlPath.substring(index + 1); + const videoId = urlPath.substring(0, index); + const { context, page } = videoById(this._traceModel, videoId); + const videoFilePath = path.join(path.dirname(context.filePath), page.video!.video.fileName); + return videoFilePath + '-' + tile; + } + + async render(videoId: string): Promise { + const { context, page } = videoById(this._traceModel, videoId); + const video = page.video!.video; + const videoFilePath = path.join(path.dirname(context.filePath), video.fileName); + const metaInfoFilePath = videoFilePath + '-metainfo.txt'; + try { + const metaInfo = await fsReadFileAsync(metaInfoFilePath, 'utf8'); + return metaInfo ? JSON.parse(metaInfo) : undefined; + } catch (e) { + } + + const ffmpeg = ffmpegExecutable()!; + console.log('Generating frames for ' + videoFilePath); // eslint-disable-line no-console + // Force output frame rate to 25 fps as otherwise it would produce one image per timebase unit + // which is currently 1 / (25 * 1000). + const result = spawnSync(ffmpeg, ['-i', videoFilePath, '-r', '25', `${videoFilePath}-%03d.png`]); + const metaInfo = parseMetaInfo(result.stderr.toString(), video); + await fsWriteFileAsync(metaInfoFilePath, metaInfo ? JSON.stringify(metaInfo) : ''); + return metaInfo; + } +} + +function parseMetaInfo(text: string, video: PageVideoTraceEvent): VideoMetaInfo | undefined { + const lines = text.split('\n'); + let framesLine = lines.find(l => l.startsWith('frame=')); + if (!framesLine) + return; + framesLine = framesLine.substring(framesLine.lastIndexOf('frame=')); + const framesMatch = framesLine.match(/frame=\s+(\d+)/); + const outputLineIndex = lines.findIndex(l => l.trim().startsWith('Output #0')); + const streamLine = lines.slice(outputLineIndex).find(l => l.trim().startsWith('Stream #0:0'))!; + const fpsMatch = streamLine.match(/, (\d+) fps,/); + const resolutionMatch = streamLine.match(/, (\d+)x(\d+)\D/); + const durationMatch = lines.find(l => l.trim().startsWith('Duration'))!.match(/Duration: (\d+):(\d\d):(\d\d.\d\d)/); + const duration = (((parseInt(durationMatch![1], 10) * 60) + parseInt(durationMatch![2], 10)) * 60 + parseFloat(durationMatch![3])) * 1000; + return { + frames: parseInt(framesMatch![1], 10), + width: parseInt(resolutionMatch![1], 10), + height: parseInt(resolutionMatch![2], 10), + fps: parseInt(fpsMatch![1], 10), + startTime: (video as any).timestamp, + endTime: (video as any).timestamp + duration + }; +} diff --git a/src/cli/traceViewer/web/common.css b/src/cli/traceViewer/web/common.css new file mode 100644 index 0000000000..b5b20b52a0 --- /dev/null +++ b/src/cli/traceViewer/web/common.css @@ -0,0 +1,122 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +:root { + --light-background: #f3f2f1; + --background: #edebe9; + --active-background: #333333; + --color: #252423; + --red: #F44336; + --green: #4CAF50; + --purple: #9C27B0; + --yellow: #FFC107; + --blue: #2196F3; + --orange: #d24726; + --black: #1E1E1E; + --gray: #888888; + --separator: #80808059; + --focus-ring: #0E639CCC; + --inactive-focus-ring: #80808059; + --layout-gap: 10px; + --selection: #074771; + --control-background: #3C3C3C; + --settings: #E7E7E7; + --sidebar-width: 250px; + --light-pink: #ff69b460; + --box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px; +} + +html, body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + overflow: hidden; + display: flex; + background: var(--background); + overscroll-behavior-x: none; +} + +* { + box-sizing: border-box; + min-width: 0; + min-height: 0; +} + +*[hidden] { + display: none !important; +} + +.codicon { + color: #C5C5C5; +} + +svg { + fill: currentColor; +} + +body { + background-color: var(--background); + color: var(--color); + font-size: 14px; + font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif; + -webkit-font-smoothing: antialiased; +} + +#root { + width: 100%; + height: 100%; + display: flex; +} + +.platform-windows { + --monospace-font: Consolas, Inconsolata, "Courier New", monospace; +} + +.platform-linux { + --monospace-font:"Droid Sans Mono", Inconsolata, "Courier New", monospace, "Droid Sans Fallback"; +} + +.platform-mac { + --monospace-font: "SF Mono",Monaco,Menlo,Inconsolata,"Courier New",monospace; +} + +.vbox { + display: flex; + flex-direction: column; + flex: auto; + position: relative; +} + +.hbox { + display: flex; + flex: auto; + position: relative; +} + +::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +::-webkit-scrollbar-thumb { + border: 1px solid #ccc; + background-color: var(--light-background); +} + +::-webkit-scrollbar-corner { + background-color: var(--background); +} diff --git a/src/cli/traceViewer/web/geometry.ts b/src/cli/traceViewer/web/geometry.ts new file mode 100644 index 0000000000..17706a0489 --- /dev/null +++ b/src/cli/traceViewer/web/geometry.ts @@ -0,0 +1,25 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +export type Size = { + width: number; + height: number; +}; + +export type Boundaries = { + minimum: number; + maximum: number; +}; diff --git a/src/cli/traceViewer/web/index.html b/src/cli/traceViewer/web/index.html new file mode 100644 index 0000000000..7c1a930d2c --- /dev/null +++ b/src/cli/traceViewer/web/index.html @@ -0,0 +1,27 @@ + + + + + + + + Playwright Trace Viewer + + +
+ + diff --git a/src/cli/traceViewer/web/index.tsx b/src/cli/traceViewer/web/index.tsx new file mode 100644 index 0000000000..b0f7b89aa2 --- /dev/null +++ b/src/cli/traceViewer/web/index.tsx @@ -0,0 +1,57 @@ +1;/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TraceModel, VideoMetaInfo } from '../traceModel'; +import './common.css'; +import './third_party/vscode/codicon.css'; +import { Workbench } from './ui/workbench'; +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { ActionTraceEvent } from '../../../trace/traceTypes'; + +declare global { + interface Window { + getTraceModel(): Promise; + getVideoMetaInfo(videoId: string): Promise; + readFile(filePath: string): Promise; + renderSnapshot(action: ActionTraceEvent): void; + } +} + +function platformName(): string { + if (window.navigator.userAgent.includes('Linux')) + return 'platform-linux'; + if (window.navigator.userAgent.includes('Windows')) + return 'platform-windows'; + if (window.navigator.userAgent.includes('Mac')) + return 'platform-mac'; + return 'platform-generic'; +} + +(async () => { + document!.defaultView!.addEventListener('focus', (event: any) => { + if (event.target.document.nodeType === Node.DOCUMENT_NODE) + document.body.classList.remove('inactive'); + }, false); + document!.defaultView!.addEventListener('blur', event => { + document.body.classList.add('inactive'); + }, false); + + document.documentElement.classList.add(platformName()); + + const traceModel = await window.getTraceModel(); + ReactDOM.render(, document.querySelector('#root')); +})(); diff --git a/src/cli/traceViewer/web/third_party/vscode/LICENSE.txt b/src/cli/traceViewer/web/third_party/vscode/LICENSE.txt new file mode 100644 index 0000000000..293c59e320 --- /dev/null +++ b/src/cli/traceViewer/web/third_party/vscode/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 - present Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/cli/traceViewer/web/third_party/vscode/codicon.css b/src/cli/traceViewer/web/third_party/vscode/codicon.css new file mode 100644 index 0000000000..2e74c00a73 --- /dev/null +++ b/src/cli/traceViewer/web/third_party/vscode/codicon.css @@ -0,0 +1,440 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +@font-face { + font-family: "codicon"; + src: url("codicon.ttf") format("truetype"); +} + +.codicon { + font: normal normal normal 16px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.codicon-add:before { content: '\ea60'; } +.codicon-plus:before { content: '\ea60'; } +.codicon-gist-new:before { content: '\ea60'; } +.codicon-repo-create:before { content: '\ea60'; } +.codicon-lightbulb:before { content: '\ea61'; } +.codicon-light-bulb:before { content: '\ea61'; } +.codicon-repo:before { content: '\ea62'; } +.codicon-repo-delete:before { content: '\ea62'; } +.codicon-gist-fork:before { content: '\ea63'; } +.codicon-repo-forked:before { content: '\ea63'; } +.codicon-git-pull-request:before { content: '\ea64'; } +.codicon-git-pull-request-abandoned:before { content: '\ea64'; } +.codicon-record-keys:before { content: '\ea65'; } +.codicon-keyboard:before { content: '\ea65'; } +.codicon-tag:before { content: '\ea66'; } +.codicon-tag-add:before { content: '\ea66'; } +.codicon-tag-remove:before { content: '\ea66'; } +.codicon-person:before { content: '\ea67'; } +.codicon-person-add:before { content: '\ea67'; } +.codicon-person-follow:before { content: '\ea67'; } +.codicon-person-outline:before { content: '\ea67'; } +.codicon-person-filled:before { content: '\ea67'; } +.codicon-git-branch:before { content: '\ea68'; } +.codicon-git-branch-create:before { content: '\ea68'; } +.codicon-git-branch-delete:before { content: '\ea68'; } +.codicon-source-control:before { content: '\ea68'; } +.codicon-mirror:before { content: '\ea69'; } +.codicon-mirror-public:before { content: '\ea69'; } +.codicon-star:before { content: '\ea6a'; } +.codicon-star-add:before { content: '\ea6a'; } +.codicon-star-delete:before { content: '\ea6a'; } +.codicon-star-empty:before { content: '\ea6a'; } +.codicon-comment:before { content: '\ea6b'; } +.codicon-comment-add:before { content: '\ea6b'; } +.codicon-alert:before { content: '\ea6c'; } +.codicon-warning:before { content: '\ea6c'; } +.codicon-search:before { content: '\ea6d'; } +.codicon-search-save:before { content: '\ea6d'; } +.codicon-log-out:before { content: '\ea6e'; } +.codicon-sign-out:before { content: '\ea6e'; } +.codicon-log-in:before { content: '\ea6f'; } +.codicon-sign-in:before { content: '\ea6f'; } +.codicon-eye:before { content: '\ea70'; } +.codicon-eye-unwatch:before { content: '\ea70'; } +.codicon-eye-watch:before { content: '\ea70'; } +.codicon-circle-filled:before { content: '\ea71'; } +.codicon-primitive-dot:before { content: '\ea71'; } +.codicon-close-dirty:before { content: '\ea71'; } +.codicon-debug-breakpoint:before { content: '\ea71'; } +.codicon-debug-breakpoint-disabled:before { content: '\ea71'; } +.codicon-debug-hint:before { content: '\ea71'; } +.codicon-primitive-square:before { content: '\ea72'; } +.codicon-edit:before { content: '\ea73'; } +.codicon-pencil:before { content: '\ea73'; } +.codicon-info:before { content: '\ea74'; } +.codicon-issue-opened:before { content: '\ea74'; } +.codicon-gist-private:before { content: '\ea75'; } +.codicon-git-fork-private:before { content: '\ea75'; } +.codicon-lock:before { content: '\ea75'; } +.codicon-mirror-private:before { content: '\ea75'; } +.codicon-close:before { content: '\ea76'; } +.codicon-remove-close:before { content: '\ea76'; } +.codicon-x:before { content: '\ea76'; } +.codicon-repo-sync:before { content: '\ea77'; } +.codicon-sync:before { content: '\ea77'; } +.codicon-clone:before { content: '\ea78'; } +.codicon-desktop-download:before { content: '\ea78'; } +.codicon-beaker:before { content: '\ea79'; } +.codicon-microscope:before { content: '\ea79'; } +.codicon-vm:before { content: '\ea7a'; } +.codicon-device-desktop:before { content: '\ea7a'; } +.codicon-file:before { content: '\ea7b'; } +.codicon-file-text:before { content: '\ea7b'; } +.codicon-more:before { content: '\ea7c'; } +.codicon-ellipsis:before { content: '\ea7c'; } +.codicon-kebab-horizontal:before { content: '\ea7c'; } +.codicon-mail-reply:before { content: '\ea7d'; } +.codicon-reply:before { content: '\ea7d'; } +.codicon-organization:before { content: '\ea7e'; } +.codicon-organization-filled:before { content: '\ea7e'; } +.codicon-organization-outline:before { content: '\ea7e'; } +.codicon-new-file:before { content: '\ea7f'; } +.codicon-file-add:before { content: '\ea7f'; } +.codicon-new-folder:before { content: '\ea80'; } +.codicon-file-directory-create:before { content: '\ea80'; } +.codicon-trash:before { content: '\ea81'; } +.codicon-trashcan:before { content: '\ea81'; } +.codicon-history:before { content: '\ea82'; } +.codicon-clock:before { content: '\ea82'; } +.codicon-folder:before { content: '\ea83'; } +.codicon-file-directory:before { content: '\ea83'; } +.codicon-symbol-folder:before { content: '\ea83'; } +.codicon-logo-github:before { content: '\ea84'; } +.codicon-mark-github:before { content: '\ea84'; } +.codicon-github:before { content: '\ea84'; } +.codicon-terminal:before { content: '\ea85'; } +.codicon-console:before { content: '\ea85'; } +.codicon-repl:before { content: '\ea85'; } +.codicon-zap:before { content: '\ea86'; } +.codicon-symbol-event:before { content: '\ea86'; } +.codicon-error:before { content: '\ea87'; } +.codicon-stop:before { content: '\ea87'; } +.codicon-variable:before { content: '\ea88'; } +.codicon-symbol-variable:before { content: '\ea88'; } +.codicon-array:before { content: '\ea8a'; } +.codicon-symbol-array:before { content: '\ea8a'; } +.codicon-symbol-module:before { content: '\ea8b'; } +.codicon-symbol-package:before { content: '\ea8b'; } +.codicon-symbol-namespace:before { content: '\ea8b'; } +.codicon-symbol-object:before { content: '\ea8b'; } +.codicon-symbol-method:before { content: '\ea8c'; } +.codicon-symbol-function:before { content: '\ea8c'; } +.codicon-symbol-constructor:before { content: '\ea8c'; } +.codicon-symbol-boolean:before { content: '\ea8f'; } +.codicon-symbol-null:before { content: '\ea8f'; } +.codicon-symbol-numeric:before { content: '\ea90'; } +.codicon-symbol-number:before { content: '\ea90'; } +.codicon-symbol-structure:before { content: '\ea91'; } +.codicon-symbol-struct:before { content: '\ea91'; } +.codicon-symbol-parameter:before { content: '\ea92'; } +.codicon-symbol-type-parameter:before { content: '\ea92'; } +.codicon-symbol-key:before { content: '\ea93'; } +.codicon-symbol-text:before { content: '\ea93'; } +.codicon-symbol-reference:before { content: '\ea94'; } +.codicon-go-to-file:before { content: '\ea94'; } +.codicon-symbol-enum:before { content: '\ea95'; } +.codicon-symbol-value:before { content: '\ea95'; } +.codicon-symbol-ruler:before { content: '\ea96'; } +.codicon-symbol-unit:before { content: '\ea96'; } +.codicon-activate-breakpoints:before { content: '\ea97'; } +.codicon-archive:before { content: '\ea98'; } +.codicon-arrow-both:before { content: '\ea99'; } +.codicon-arrow-down:before { content: '\ea9a'; } +.codicon-arrow-left:before { content: '\ea9b'; } +.codicon-arrow-right:before { content: '\ea9c'; } +.codicon-arrow-small-down:before { content: '\ea9d'; } +.codicon-arrow-small-left:before { content: '\ea9e'; } +.codicon-arrow-small-right:before { content: '\ea9f'; } +.codicon-arrow-small-up:before { content: '\eaa0'; } +.codicon-arrow-up:before { content: '\eaa1'; } +.codicon-bell:before { content: '\eaa2'; } +.codicon-bold:before { content: '\eaa3'; } +.codicon-book:before { content: '\eaa4'; } +.codicon-bookmark:before { content: '\eaa5'; } +.codicon-debug-breakpoint-conditional-unverified:before { content: '\eaa6'; } +.codicon-debug-breakpoint-conditional:before { content: '\eaa7'; } +.codicon-debug-breakpoint-conditional-disabled:before { content: '\eaa7'; } +.codicon-debug-breakpoint-data-unverified:before { content: '\eaa8'; } +.codicon-debug-breakpoint-data:before { content: '\eaa9'; } +.codicon-debug-breakpoint-data-disabled:before { content: '\eaa9'; } +.codicon-debug-breakpoint-log-unverified:before { content: '\eaaa'; } +.codicon-debug-breakpoint-log:before { content: '\eaab'; } +.codicon-debug-breakpoint-log-disabled:before { content: '\eaab'; } +.codicon-briefcase:before { content: '\eaac'; } +.codicon-broadcast:before { content: '\eaad'; } +.codicon-browser:before { content: '\eaae'; } +.codicon-bug:before { content: '\eaaf'; } +.codicon-calendar:before { content: '\eab0'; } +.codicon-case-sensitive:before { content: '\eab1'; } +.codicon-check:before { content: '\eab2'; } +.codicon-checklist:before { content: '\eab3'; } +.codicon-chevron-down:before { content: '\eab4'; } +.codicon-chevron-left:before { content: '\eab5'; } +.codicon-chevron-right:before { content: '\eab6'; } +.codicon-chevron-up:before { content: '\eab7'; } +.codicon-chrome-close:before { content: '\eab8'; } +.codicon-chrome-maximize:before { content: '\eab9'; } +.codicon-chrome-minimize:before { content: '\eaba'; } +.codicon-chrome-restore:before { content: '\eabb'; } +.codicon-circle-outline:before { content: '\eabc'; } +.codicon-debug-breakpoint-unverified:before { content: '\eabc'; } +.codicon-circle-slash:before { content: '\eabd'; } +.codicon-circuit-board:before { content: '\eabe'; } +.codicon-clear-all:before { content: '\eabf'; } +.codicon-clippy:before { content: '\eac0'; } +.codicon-close-all:before { content: '\eac1'; } +.codicon-cloud-download:before { content: '\eac2'; } +.codicon-cloud-upload:before { content: '\eac3'; } +.codicon-code:before { content: '\eac4'; } +.codicon-collapse-all:before { content: '\eac5'; } +.codicon-color-mode:before { content: '\eac6'; } +.codicon-comment-discussion:before { content: '\eac7'; } +.codicon-compare-changes:before { content: '\eafd'; } +.codicon-credit-card:before { content: '\eac9'; } +.codicon-dash:before { content: '\eacc'; } +.codicon-dashboard:before { content: '\eacd'; } +.codicon-database:before { content: '\eace'; } +.codicon-debug-continue:before { content: '\eacf'; } +.codicon-debug-disconnect:before { content: '\ead0'; } +.codicon-debug-pause:before { content: '\ead1'; } +.codicon-debug-restart:before { content: '\ead2'; } +.codicon-debug-start:before { content: '\ead3'; } +.codicon-debug-step-into:before { content: '\ead4'; } +.codicon-debug-step-out:before { content: '\ead5'; } +.codicon-debug-step-over:before { content: '\ead6'; } +.codicon-debug-stop:before { content: '\ead7'; } +.codicon-debug:before { content: '\ead8'; } +.codicon-device-camera-video:before { content: '\ead9'; } +.codicon-device-camera:before { content: '\eada'; } +.codicon-device-mobile:before { content: '\eadb'; } +.codicon-diff-added:before { content: '\eadc'; } +.codicon-diff-ignored:before { content: '\eadd'; } +.codicon-diff-modified:before { content: '\eade'; } +.codicon-diff-removed:before { content: '\eadf'; } +.codicon-diff-renamed:before { content: '\eae0'; } +.codicon-diff:before { content: '\eae1'; } +.codicon-discard:before { content: '\eae2'; } +.codicon-editor-layout:before { content: '\eae3'; } +.codicon-empty-window:before { content: '\eae4'; } +.codicon-exclude:before { content: '\eae5'; } +.codicon-extensions:before { content: '\eae6'; } +.codicon-eye-closed:before { content: '\eae7'; } +.codicon-file-binary:before { content: '\eae8'; } +.codicon-file-code:before { content: '\eae9'; } +.codicon-file-media:before { content: '\eaea'; } +.codicon-file-pdf:before { content: '\eaeb'; } +.codicon-file-submodule:before { content: '\eaec'; } +.codicon-file-symlink-directory:before { content: '\eaed'; } +.codicon-file-symlink-file:before { content: '\eaee'; } +.codicon-file-zip:before { content: '\eaef'; } +.codicon-files:before { content: '\eaf0'; } +.codicon-filter:before { content: '\eaf1'; } +.codicon-flame:before { content: '\eaf2'; } +.codicon-fold-down:before { content: '\eaf3'; } +.codicon-fold-up:before { content: '\eaf4'; } +.codicon-fold:before { content: '\eaf5'; } +.codicon-folder-active:before { content: '\eaf6'; } +.codicon-folder-opened:before { content: '\eaf7'; } +.codicon-gear:before { content: '\eaf8'; } +.codicon-gift:before { content: '\eaf9'; } +.codicon-gist-secret:before { content: '\eafa'; } +.codicon-gist:before { content: '\eafb'; } +.codicon-git-commit:before { content: '\eafc'; } +.codicon-git-compare:before { content: '\eafd'; } +.codicon-git-merge:before { content: '\eafe'; } +.codicon-github-action:before { content: '\eaff'; } +.codicon-github-alt:before { content: '\eb00'; } +.codicon-globe:before { content: '\eb01'; } +.codicon-grabber:before { content: '\eb02'; } +.codicon-graph:before { content: '\eb03'; } +.codicon-gripper:before { content: '\eb04'; } +.codicon-heart:before { content: '\eb05'; } +.codicon-home:before { content: '\eb06'; } +.codicon-horizontal-rule:before { content: '\eb07'; } +.codicon-hubot:before { content: '\eb08'; } +.codicon-inbox:before { content: '\eb09'; } +.codicon-issue-closed:before { content: '\eb0a'; } +.codicon-issue-reopened:before { content: '\eb0b'; } +.codicon-issues:before { content: '\eb0c'; } +.codicon-italic:before { content: '\eb0d'; } +.codicon-jersey:before { content: '\eb0e'; } +.codicon-json:before { content: '\eb0f'; } +.codicon-kebab-vertical:before { content: '\eb10'; } +.codicon-key:before { content: '\eb11'; } +.codicon-law:before { content: '\eb12'; } +.codicon-lightbulb-autofix:before { content: '\eb13'; } +.codicon-link-external:before { content: '\eb14'; } +.codicon-link:before { content: '\eb15'; } +.codicon-list-ordered:before { content: '\eb16'; } +.codicon-list-unordered:before { content: '\eb17'; } +.codicon-live-share:before { content: '\eb18'; } +.codicon-loading:before { content: '\eb19'; } +.codicon-location:before { content: '\eb1a'; } +.codicon-mail-read:before { content: '\eb1b'; } +.codicon-mail:before { content: '\eb1c'; } +.codicon-markdown:before { content: '\eb1d'; } +.codicon-megaphone:before { content: '\eb1e'; } +.codicon-mention:before { content: '\eb1f'; } +.codicon-milestone:before { content: '\eb20'; } +.codicon-mortar-board:before { content: '\eb21'; } +.codicon-move:before { content: '\eb22'; } +.codicon-multiple-windows:before { content: '\eb23'; } +.codicon-mute:before { content: '\eb24'; } +.codicon-no-newline:before { content: '\eb25'; } +.codicon-note:before { content: '\eb26'; } +.codicon-octoface:before { content: '\eb27'; } +.codicon-open-preview:before { content: '\eb28'; } +.codicon-package:before { content: '\eb29'; } +.codicon-paintcan:before { content: '\eb2a'; } +.codicon-pin:before { content: '\eb2b'; } +.codicon-play:before { content: '\eb2c'; } +.codicon-run:before { content: '\eb2c'; } +.codicon-plug:before { content: '\eb2d'; } +.codicon-preserve-case:before { content: '\eb2e'; } +.codicon-preview:before { content: '\eb2f'; } +.codicon-project:before { content: '\eb30'; } +.codicon-pulse:before { content: '\eb31'; } +.codicon-question:before { content: '\eb32'; } +.codicon-quote:before { content: '\eb33'; } +.codicon-radio-tower:before { content: '\eb34'; } +.codicon-reactions:before { content: '\eb35'; } +.codicon-references:before { content: '\eb36'; } +.codicon-refresh:before { content: '\eb37'; } +.codicon-regex:before { content: '\eb38'; } +.codicon-remote-explorer:before { content: '\eb39'; } +.codicon-remote:before { content: '\eb3a'; } +.codicon-remove:before { content: '\eb3b'; } +.codicon-replace-all:before { content: '\eb3c'; } +.codicon-replace:before { content: '\eb3d'; } +.codicon-repo-clone:before { content: '\eb3e'; } +.codicon-repo-force-push:before { content: '\eb3f'; } +.codicon-repo-pull:before { content: '\eb40'; } +.codicon-repo-push:before { content: '\eb41'; } +.codicon-report:before { content: '\eb42'; } +.codicon-request-changes:before { content: '\eb43'; } +.codicon-rocket:before { content: '\eb44'; } +.codicon-root-folder-opened:before { content: '\eb45'; } +.codicon-root-folder:before { content: '\eb46'; } +.codicon-rss:before { content: '\eb47'; } +.codicon-ruby:before { content: '\eb48'; } +.codicon-save-all:before { content: '\eb49'; } +.codicon-save-as:before { content: '\eb4a'; } +.codicon-save:before { content: '\eb4b'; } +.codicon-screen-full:before { content: '\eb4c'; } +.codicon-screen-normal:before { content: '\eb4d'; } +.codicon-search-stop:before { content: '\eb4e'; } +.codicon-server:before { content: '\eb50'; } +.codicon-settings-gear:before { content: '\eb51'; } +.codicon-settings:before { content: '\eb52'; } +.codicon-shield:before { content: '\eb53'; } +.codicon-smiley:before { content: '\eb54'; } +.codicon-sort-precedence:before { content: '\eb55'; } +.codicon-split-horizontal:before { content: '\eb56'; } +.codicon-split-vertical:before { content: '\eb57'; } +.codicon-squirrel:before { content: '\eb58'; } +.codicon-star-full:before { content: '\eb59'; } +.codicon-star-half:before { content: '\eb5a'; } +.codicon-symbol-class:before { content: '\eb5b'; } +.codicon-symbol-color:before { content: '\eb5c'; } +.codicon-symbol-constant:before { content: '\eb5d'; } +.codicon-symbol-enum-member:before { content: '\eb5e'; } +.codicon-symbol-field:before { content: '\eb5f'; } +.codicon-symbol-file:before { content: '\eb60'; } +.codicon-symbol-interface:before { content: '\eb61'; } +.codicon-symbol-keyword:before { content: '\eb62'; } +.codicon-symbol-misc:before { content: '\eb63'; } +.codicon-symbol-operator:before { content: '\eb64'; } +.codicon-symbol-property:before { content: '\eb65'; } +.codicon-wrench:before { content: '\eb65'; } +.codicon-wrench-subaction:before { content: '\eb65'; } +.codicon-symbol-snippet:before { content: '\eb66'; } +.codicon-tasklist:before { content: '\eb67'; } +.codicon-telescope:before { content: '\eb68'; } +.codicon-text-size:before { content: '\eb69'; } +.codicon-three-bars:before { content: '\eb6a'; } +.codicon-thumbsdown:before { content: '\eb6b'; } +.codicon-thumbsup:before { content: '\eb6c'; } +.codicon-tools:before { content: '\eb6d'; } +.codicon-triangle-down:before { content: '\eb6e'; } +.codicon-triangle-left:before { content: '\eb6f'; } +.codicon-triangle-right:before { content: '\eb70'; } +.codicon-triangle-up:before { content: '\eb71'; } +.codicon-twitter:before { content: '\eb72'; } +.codicon-unfold:before { content: '\eb73'; } +.codicon-unlock:before { content: '\eb74'; } +.codicon-unmute:before { content: '\eb75'; } +.codicon-unverified:before { content: '\eb76'; } +.codicon-verified:before { content: '\eb77'; } +.codicon-versions:before { content: '\eb78'; } +.codicon-vm-active:before { content: '\eb79'; } +.codicon-vm-outline:before { content: '\eb7a'; } +.codicon-vm-running:before { content: '\eb7b'; } +.codicon-watch:before { content: '\eb7c'; } +.codicon-whitespace:before { content: '\eb7d'; } +.codicon-whole-word:before { content: '\eb7e'; } +.codicon-window:before { content: '\eb7f'; } +.codicon-word-wrap:before { content: '\eb80'; } +.codicon-zoom-in:before { content: '\eb81'; } +.codicon-zoom-out:before { content: '\eb82'; } +.codicon-list-filter:before { content: '\eb83'; } +.codicon-list-flat:before { content: '\eb84'; } +.codicon-list-selection:before { content: '\eb85'; } +.codicon-selection:before { content: '\eb85'; } +.codicon-list-tree:before { content: '\eb86'; } +.codicon-debug-breakpoint-function-unverified:before { content: '\eb87'; } +.codicon-debug-breakpoint-function:before { content: '\eb88'; } +.codicon-debug-breakpoint-function-disabled:before { content: '\eb88'; } +.codicon-debug-stackframe-active:before { content: '\eb89'; } +.codicon-debug-stackframe-dot:before { content: '\eb8a'; } +.codicon-debug-stackframe:before { content: '\eb8b'; } +.codicon-debug-stackframe-focused:before { content: '\eb8b'; } +.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; } +.codicon-symbol-string:before { content: '\eb8d'; } +.codicon-debug-reverse-continue:before { content: '\eb8e'; } +.codicon-debug-step-back:before { content: '\eb8f'; } +.codicon-debug-restart-frame:before { content: '\eb90'; } +.codicon-call-incoming:before { content: '\eb92'; } +.codicon-call-outgoing:before { content: '\eb93'; } +.codicon-menu:before { content: '\eb94'; } +.codicon-expand-all:before { content: '\eb95'; } +.codicon-feedback:before { content: '\eb96'; } +.codicon-group-by-ref-type:before { content: '\eb97'; } +.codicon-ungroup-by-ref-type:before { content: '\eb98'; } +.codicon-account:before { content: '\eb99'; } +.codicon-bell-dot:before { content: '\eb9a'; } +.codicon-debug-console:before { content: '\eb9b'; } +.codicon-library:before { content: '\eb9c'; } +.codicon-output:before { content: '\eb9d'; } +.codicon-run-all:before { content: '\eb9e'; } +.codicon-sync-ignored:before { content: '\eb9f'; } +.codicon-pinned:before { content: '\eba0'; } +.codicon-github-inverted:before { content: '\eba1'; } +.codicon-debug-alt:before { content: '\eb91'; } +.codicon-server-process:before { content: '\eba2'; } +.codicon-server-environment:before { content: '\eba3'; } +.codicon-pass:before { content: '\eba4'; } +.codicon-stop-circle:before { content: '\eba5'; } +.codicon-play-circle:before { content: '\eba6'; } +.codicon-record:before { content: '\eba7'; } +.codicon-debug-alt-small:before { content: '\eba8'; } +.codicon-vm-connect:before { content: '\eba9'; } +.codicon-cloud:before { content: '\ebaa'; } +.codicon-merge:before { content: '\ebab'; } +.codicon-export:before { content: '\ebac'; } +.codicon-graph-left:before { content: '\ebad'; } +.codicon-magnet:before { content: '\ebae'; } diff --git a/src/cli/traceViewer/web/third_party/vscode/codicon.ttf b/src/cli/traceViewer/web/third_party/vscode/codicon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..82acc8995b8d7ee0338bc65547844f5f99b953ce GIT binary patch literal 61024 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 z zIxk(X6Ar~^*VN?u-LJ$6c>Sqd=I%-MK=OKrI1@<(sUcwZb5m&kRQ`s_j*%-b9}Xs} zuDJW2i`NPdt>aI&MqEAhxx0=;6Xj!9Trrqpzs%or`GK=ae47@9R_zId`)?fEPoBj<^i#f45p>n(p$VFi; zO!x)avJd18tQ;U^4g!XO=1l)TXMNSuljqjvguhDc1~x{V7KMLdIcutu{v{W^W?+h_ z_zU{q0LFNV^z)*J&>WO_JwX!8{}6Rb>BbSH%Ufm=Y@ce;UQX^Au{^0LXJqj#8|S-n5CfB8P~gjxE2#SU}vIr^dw^FXMh zxMELnh4l62)~3$eH{P~E`mZ!wkdiKyUU_DQbx0rnX}(?xHgj0>3KE*jF!+4&_Gflg zDSGNu^qccy`Sfe#4!=M4X85i6w{XFH8S+?B%Es!!6-5YOcS2Mr-9XLqK* zVy-RC^FN|3@!)5edVu4K#?eF&QE-Sn zfcmMTPQiy;v_LMkFi`_UB+2vi0xTb7TfA5B9m&6eIK*Fo zIV#~CgTuy>Qs1CtfV8u3`&vQv&^%oNmp6gTrZsV*4hn*=E3^qZ7mxd zP}VCokyvG!+bf7#RiLijkDP=aM9jqFZf7*Ov;=L^sS7V|DDim>fdIILK_)Hh9kPwi z&M}P?`vwDJ#0jVq5spQWE*&9Yg#QQr0|-kZ--0^8;eu7 z-In@@ZhmI28F+Qa9HZQSk%AViXiYWt3;PA|oMm-hZ&}1%N6kWJ8YFRP7 z=p)8!3d)jgL9*}63Z0c$l0Jza4Pp&$TRCuBT~2H8Z$RQ9dkSB2{S9l{jfuKA`0K;5 zx&*r*=!@gW7n}l{y}qVI`1n22@iu5JwTN}<#(0UL8#&*bB5(_#6#+*CiAyrK92hGYIYBaj zkqq#8m`Dk21uqM5E=?F(MM}2mASOhbQa7d)k;X_pWO;qKOhxEW_Vf7AErH=7|7}C@ zfjffx_66_IU8z2IxAsc+IeT1}>(A}hUS8C7&Qhg{wFm}<+2yx-!*0F7=Cr6(NrTmC zHGl$HuhBYf4awxjVgIeev7y`j`}PHH8;T9z>Q7zaI=9PpMBCM+J>u#**L8*V+;iCh zjYB7hz9OC70;r)4c22+7QzA{U(+wCqg>2z8n$h@Q%lZ8H3D39j z&Q~?Yev5=7vA^$C27XI{YzSqhV*GZq1^MFM#u@xzRJ zCIts&S86&cdMTtKDIgF|AAiC}mgSam%39FUHf&2Iwq>>^ zKy$Cz=|!mS-4w0sB_!wHKJWwibFRuQg*`g4tq-Se?cdtKe!im^Cmv6E5w}}tSM$%$ zBW*rbj{p6$|J~2~zj{Wwem~^7#qH?-nC*Wg2D(tD$cYOrjnI0~urRZQa+ZQJ?T$D< z;kn9?+1=tX8Z~9{l4zzbFu&IJ947mpP=vPN-yWGiuk3>hVCj_QHtF-+Zz-dFV7`Qj z2et+)p>^a?A+~)Q)GllrN5{WMZug^ps4 X1|Ou=Q(*tda06I>Ma-t5~M?57D~u< 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 void, +}> = ({ actions, selectedAction, onSelected }) => { + return
{actions.map(actionEntry => { + const { action, actionId } = actionEntry; + return
onSelected(actionEntry)}> +
+
{action.action}
+ {action.selector &&
{action.selector}
} + {action.action === 'goto' && action.value &&
{action.value}
} +
+
+ {action.snapshot ? : 'No snapshot available'} +
+
; + })}
; +}; diff --git a/src/cli/traceViewer/web/ui/contextSelector.css b/src/cli/traceViewer/web/ui/contextSelector.css new file mode 100644 index 0000000000..7f11adaaa0 --- /dev/null +++ b/src/cli/traceViewer/web/ui/contextSelector.css @@ -0,0 +1,30 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.context-selector { + min-width: 38px; + border: 1px solid transparent; + border-radius: 0; + padding: 0 13px 0 5px; + position: relative; + height: 22px; + align-self: center; + margin-right: 20px; + background: rgba(222, 222, 222, 0.3); + color: white; + border-radius: 3px; + outline: none !important; +} diff --git a/src/cli/traceViewer/web/ui/contextSelector.tsx b/src/cli/traceViewer/web/ui/contextSelector.tsx new file mode 100644 index 0000000000..cf83feb526 --- /dev/null +++ b/src/cli/traceViewer/web/ui/contextSelector.tsx @@ -0,0 +1,41 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; +import { ContextEntry } from '../../traceModel'; +import './contextSelector.css'; + +export const ContextSelector: React.FunctionComponent<{ + contexts: ContextEntry[], + context: ContextEntry, + onChange: (contextEntry: ContextEntry) => void, +}> = ({ contexts, context, onChange }) => { + return ( + + ); +}; diff --git a/src/cli/traceViewer/web/ui/filmStrip.css b/src/cli/traceViewer/web/ui/filmStrip.css new file mode 100644 index 0000000000..d0169adceb --- /dev/null +++ b/src/cli/traceViewer/web/ui/filmStrip.css @@ -0,0 +1,45 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.film-strip { + flex: none; + display: flex; + flex-direction: column; + position: relative; +} + +.film-strip-lane { + flex: none; + display: flex; +} + +.film-strip-frame { + flex: none; + pointer-events: none; + box-shadow: var(--box-shadow); +} + +.film-strip-hover { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: white; + box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px; + box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 10px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 10px 0px; + z-index: 10; +} diff --git a/src/cli/traceViewer/web/ui/filmStrip.tsx b/src/cli/traceViewer/web/ui/filmStrip.tsx new file mode 100644 index 0000000000..5d91b809f5 --- /dev/null +++ b/src/cli/traceViewer/web/ui/filmStrip.tsx @@ -0,0 +1,136 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ContextEntry, VideoEntry, VideoMetaInfo } from '../../traceModel'; +import './filmStrip.css'; +import { Boundaries } from '../geometry'; +import * as React from 'react'; +import { useAsyncMemo, useMeasure } from './helpers'; + +function imageURL(videoId: string, index: number) { + const imageURLpadding = '0'.repeat(3 - String(index + 1).length); + return `video-tile/${videoId}/${imageURLpadding}${index + 1}.png`; +} + +export const FilmStrip: React.FunctionComponent<{ + context: ContextEntry, + boundaries: Boundaries, + previewX?: number, +}> = ({ context, boundaries, previewX }) => { + const [measure, ref] = useMeasure(); + + const videos = React.useMemo(() => { + const videos: VideoEntry[] = []; + for (const page of context.pages) { + if (page.video) + videos.push(page.video); + } + return videos; + }, [context]); + + const metaInfos = useAsyncMemo>(async () => { + const infos = new Map(); + for (const video of videos) + infos.set(video, await window.getVideoMetaInfo(video.videoId)); + return infos; + }, [videos], new Map(), new Map()); + + // TODO: pick file from the Y position. + const previewVideo = videos[0]; + const previewMetaInfo = metaInfos.get(previewVideo); + let previewIndex = 0; + if ((previewX !== undefined) && previewMetaInfo) { + const previewTime = boundaries.minimum + (boundaries.maximum - boundaries.minimum) * previewX / measure.width; + previewIndex = (previewTime - previewMetaInfo.startTime) / (previewMetaInfo.endTime - previewMetaInfo.startTime) * previewMetaInfo.frames | 0; + } + + const previewImage = useAsyncMemo(async () => { + if (!previewMetaInfo || previewIndex < 0 || previewIndex >= previewMetaInfo.frames) + return; + const idealWidth = previewMetaInfo.width / 2; + const idealHeight = previewMetaInfo.height / 2; + const ratio = Math.min(1, (measure.width - 20) / idealWidth); + const image = new Image((idealWidth * ratio) | 0, (idealHeight * ratio) | 0); + image.src = imageURL(previewVideo.videoId, previewIndex); + await new Promise(f => image.onload = f); + return image; + }, [previewMetaInfo, previewIndex, measure.width, previewVideo], undefined); + + return
{ + videos.map(video => ) + } + {(previewX !== undefined) && previewMetaInfo && previewImage && +
+ +
+ } +
; +}; + +const FilmStripLane: React.FunctionComponent<{ + boundaries: Boundaries, + video: VideoEntry, + metaInfo: VideoMetaInfo | undefined, + width: number, +}> = ({ boundaries, video, metaInfo, width }) => { + const frameHeight = 45; + const frameMargin = 2.5; + + if (!metaInfo) + return
; + + const frameWidth = frameHeight / metaInfo.height * metaInfo.width | 0; + const boundariesSize = boundaries.maximum - boundaries.minimum; + const gapLeft = (metaInfo.startTime - boundaries.minimum) / boundariesSize * width; + const gapRight = (boundaries.maximum - metaInfo.endTime) / boundariesSize * width; + const effectiveWidth = (metaInfo.endTime - metaInfo.startTime) / boundariesSize * width; + + const frameCount = effectiveWidth / (frameWidth + 2 * frameMargin) | 0; + const frameStep = metaInfo.frames / frameCount; + const frameGap = frameCount <= 1 ? 0 : (effectiveWidth - (frameWidth + 2 * frameMargin) * frameCount) / (frameCount - 1); + + const frames: JSX.Element[] = []; + for (let i = 0; i < metaInfo.frames; i += frameStep) { + let index = i | 0; + // Always show last frame. + if (Math.floor(i + frameStep) >= metaInfo.frames) + index = metaInfo.frames - 1; + frames.push(
); + } + + return
{frames}
; +}; diff --git a/src/cli/traceViewer/web/ui/helpers.tsx b/src/cli/traceViewer/web/ui/helpers.tsx new file mode 100644 index 0000000000..545911eceb --- /dev/null +++ b/src/cli/traceViewer/web/ui/helpers.tsx @@ -0,0 +1,73 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; + +// Recalculates the value when dependencies change. +export function useAsyncMemo(fn: () => Promise, deps: React.DependencyList, initialValue: T, resetValue?: T) { + const [value, setValue] = React.useState(initialValue); + React.useEffect(() => { + let canceled = false; + if (resetValue !== undefined) + setValue(resetValue); + fn().then(value => { + if (!canceled) + setValue(value); + }); + return () => { + canceled = true; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); + return value; +} + +// Tracks the element size and returns it's contentRect (always has x=0, y=0). +export function useMeasure() { + const ref = React.useRef(null); + const [measure, setMeasure] = React.useState(new DOMRect(0, 0, 10, 10)); + React.useLayoutEffect(() => { + const target = ref.current; + if (!target) + return; + const resizeObserver = new ResizeObserver(entries => { + const entry = entries[entries.length - 1]; + if (entry && entry.contentRect) + setMeasure(entry.contentRect); + }); + resizeObserver.observe(target); + return () => resizeObserver.unobserve(target); + }, [ref]); + return [measure, ref] as const; +} + +export const Expandable: React.FunctionComponent<{ + title: JSX.Element, + body: JSX.Element, + style?: React.CSSProperties, +}> = ({ title, body, style }) => { + const [expanded, setExpanded] = React.useState(true); + return
+
+
setExpanded(!expanded)} /> + {title} +
+ { expanded &&
{body}
} +
; +}; diff --git a/src/cli/traceViewer/web/ui/networkTab.css b/src/cli/traceViewer/web/ui/networkTab.css new file mode 100644 index 0000000000..d640c4bd64 --- /dev/null +++ b/src/cli/traceViewer/web/ui/networkTab.css @@ -0,0 +1,68 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.network-tab { + display: flex; + flex-direction: column; + flex: auto; + overflow: auto; +} + +.network-tab:focus { + outline: none; +} + +.network-request { + box-shadow: var(--box-shadow); + white-space: nowrap; + display: flex; + align-items: center; + padding: 0 10px; + margin-bottom: 10px; + background: #fdfcfc; + width: 100%; + border: 3px solid transparent; + flex: none; + outline: none; +} + +.network-request-title { + height: 36px; + display: flex; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; +} + +.network-request-details { + font-family: var(--monospace-font); + white-space: pre; + overflow: hidden; +} + +.network-request-title > div { + overflow: hidden; + text-overflow: ellipsis; +} + +.network-request.selected, +.network-request:hover { + border-color: var(--inactive-focus-ring); +} + +.network-request.selected:focus { + border-color: var(--orange); +} diff --git a/src/cli/traceViewer/web/ui/networkTab.tsx b/src/cli/traceViewer/web/ui/networkTab.tsx new file mode 100644 index 0000000000..cc5759c36d --- /dev/null +++ b/src/cli/traceViewer/web/ui/networkTab.tsx @@ -0,0 +1,39 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ActionEntry } from '../../traceModel'; +import './networkTab.css'; +import * as React from 'react'; +import { Expandable } from './helpers'; + +export const NetworkTab: React.FunctionComponent<{ + actionEntry: ActionEntry | undefined, +}> = ({ actionEntry }) => { + const [selected, setSelected] = React.useState(0); + return
{ + (actionEntry ? actionEntry.resources : []).map((resource, index) => { + return
setSelected(index)}> +
resource.url
+ } body={ +
{resource.responseHeaders.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
+ }/> +
; + }) + }
; +}; diff --git a/src/cli/traceViewer/web/ui/propertiesTabbedPane.css b/src/cli/traceViewer/web/ui/propertiesTabbedPane.css new file mode 100644 index 0000000000..6f2f377bd7 --- /dev/null +++ b/src/cli/traceViewer/web/ui/propertiesTabbedPane.css @@ -0,0 +1,86 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.properties-tabbed-pane { + display: flex; + flex: auto; + overflow: hidden; +} + +.properties-tab-content { + display: flex; + flex: auto; + overflow: hidden; +} + +.properties-tab-strip { + flex: auto; + display: flex; + flex-direction: row; + align-items: center; + height: 34px; +} + +.properties-tab-strip:focus { + outline: none; +} + +.properties-tab-element { + padding: 2px 6px 0 6px; + margin-right: 4px; + cursor: pointer; + display: flex; + flex: none; + align-items: center; + justify-content: center; + user-select: none; + border-bottom: 3px solid transparent; + width: 80px; + outline: none; +} + +.properties-tab-label { + max-width: 250px; + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; +} + +.properties-tab-element.selected { + border-bottom-color: var(--color); +} + +.properties-tab-element:hover { + font-weight: 600; +} + +.snapshot-wrapper { + flex: auto; + margin: 1px; +} + +.snapshot-container { + display: block; + background: white; + outline: 1px solid #aaa; +} + +iframe#snapshot { + width: 100%; + height: 100%; + border: none; +} diff --git a/src/cli/traceViewer/web/ui/propertiesTabbedPane.tsx b/src/cli/traceViewer/web/ui/propertiesTabbedPane.tsx new file mode 100644 index 0000000000..be3305b65a --- /dev/null +++ b/src/cli/traceViewer/web/ui/propertiesTabbedPane.tsx @@ -0,0 +1,88 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ActionEntry } from '../../traceModel'; +import { Size } from '../geometry'; +import { NetworkTab } from './networkTab'; +import { SourceTab } from './sourceTab'; +import './propertiesTabbedPane.css'; +import * as React from 'react'; +import { useMeasure } from './helpers'; + +export const PropertiesTabbedPane: React.FunctionComponent<{ + actionEntry: ActionEntry | undefined, + snapshotSize: Size, +}> = ({ actionEntry, snapshotSize }) => { + const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network'>('snapshot'); + return
+
+
+
+
setSelected('snapshot')}> +
Snapshot
+
+
setSelected('source')}> +
Source
+
+
setSelected('network')}> +
Network
+
+
+
+
+ +
+
+ +
+
+ +
+
+
; +}; + +const SnapshotTab: React.FunctionComponent<{ + actionEntry: ActionEntry | undefined, + snapshotSize: Size, +}> = ({ actionEntry, snapshotSize }) => { + const [measure, ref] = useMeasure(); + + const iframeRef = React.createRef(); + React.useEffect(() => { + if (iframeRef.current && !actionEntry) + iframeRef.current.src = 'about:blank'; + }, [actionEntry, iframeRef]); + + React.useEffect(() => { + if (actionEntry) + window.renderSnapshot(actionEntry.action); + }, [actionEntry]); + + const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height); + return
+
+ +
+
; +}; diff --git a/src/cli/traceViewer/web/ui/sourceTab.css b/src/cli/traceViewer/web/ui/sourceTab.css new file mode 100644 index 0000000000..f455b57e69 --- /dev/null +++ b/src/cli/traceViewer/web/ui/sourceTab.css @@ -0,0 +1,43 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.source-tab { + flex: auto; + position: relative; + overflow: auto; + background: #fdfcfc; + font-family: var(--monospace-font); + white-space: nowrap; +} + +.source-line-number { + width: 80px; + border-right: 1px solid var(--separator); + display: inline-block; + margin-right: 3px; + text-align: end; + padding-right: 4px; + color: var(--gray); +} + +.source-code { + white-space: pre; + display: inline-block; +} + +.source-line-highlight { + background-color: #ff69b460; +} diff --git a/src/cli/traceViewer/web/ui/sourceTab.tsx b/src/cli/traceViewer/web/ui/sourceTab.tsx new file mode 100644 index 0000000000..a38888c688 --- /dev/null +++ b/src/cli/traceViewer/web/ui/sourceTab.tsx @@ -0,0 +1,75 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ActionEntry } from '../../traceModel'; +import * as React from 'react'; +import { useAsyncMemo } from './helpers'; +import './sourceTab.css'; +import '../../../../third_party/highlightjs/highlightjs/tomorrow.css'; +import * as highlightjs from '../../../../third_party/highlightjs/highlightjs'; + +export const SourceTab: React.FunctionComponent<{ + actionEntry: ActionEntry | undefined, +}> = ({ actionEntry }) => { + const location = React.useMemo<{ fileName?: string, lineNumber?: number, value?: string }>(() => { + if (!actionEntry) + return { value: '' }; + const { action } = actionEntry; + const frames = action.stack!.split('\n').slice(1); + const frame = frames.filter(frame => !frame.includes('playwright/lib/') && !frame.includes('playwright/src/'))[0]; + if (!frame) + return { value: action.stack! }; + const match = frame.match(/at [^(]+\(([^:]+):(\d+):\d+\)/) || frame.match(/at ([^:^(]+):(\d+):\d+/); + if (!match) + return { value: action.stack! }; + const fileName = match[1]; + const lineNumber = parseInt(match[2], 10); + return { fileName, lineNumber }; + }, [actionEntry]); + + const content = useAsyncMemo(async () => { + const value = location.fileName ? await window.readFile(location.fileName) : location.value; + const result = []; + let continuation: any; + for (const line of (value || '').split('\n')) { + const highlighted = highlightjs.highlight('javascript', line, true, continuation); + continuation = highlighted.top; + result.push(highlighted.value); + } + return result; + }, [location.fileName, location.value], []); + + const targetLineRef = React.createRef(); + React.useLayoutEffect(() => { + if (targetLineRef.current) + targetLineRef.current.scrollIntoView({ block: 'center', inline: 'nearest' }); + }, [content, location.lineNumber, targetLineRef]); + + return
{ + content.map((markup, index) => { + const isTargetLine = (index + 1) === location.lineNumber; + return
+
{index + 1}
+
+
; + }) + } +
; +}; diff --git a/src/cli/traceViewer/web/ui/timeline.css b/src/cli/traceViewer/web/ui/timeline.css new file mode 100644 index 0000000000..24fa5b057b --- /dev/null +++ b/src/cli/traceViewer/web/ui/timeline.css @@ -0,0 +1,110 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.timeline-view { + flex: none; + flex-basis: 60px; + position: relative; + display: flex; + flex-direction: column; + background: white; + padding: 20px 0 5px; + cursor: text; +} + +.timeline-divider { + position: absolute; + width: 1px; + top: 0; + bottom: 0; + background-color: rgb(0 0 0 / 10%); +} + +.timeline-label { + position: absolute; + top: 4px; + right: 3px; + font-size: 80%; + white-space: nowrap; + pointer-events: none; +} + +.timeline-lane { + pointer-events: none; + overflow: hidden; + flex: none; + flex-basis: 20px; + position: relative; +} + +.timeline-grid { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; +} + +.timeline-lane.timeline-action-labels { + margin-top: 10px; +} + +.timeline-lane.timeline-actions { + margin-bottom: 10px; +} + +.timeline-action { + position: absolute; + top: 0; + bottom: 0; + background-color: red; + border-radius: 3px; +} + +.timeline-action.click { + background-color: var(--green); +} + +.timeline-action.fill, +.timeline-action.press { + background-color: var(--orange); +} + +.timeline-action.goto { + background-color: var(--blue); +} + +.timeline-action-label { + position: absolute; + top: 0; + bottom: 0; + margin-left: 2px; + background-color: #fffffff0; +} + +.timeline-time-bar { + display: none; + position: absolute; + top: 0; + bottom: 0; + width: 3px; + background-color: black; + pointer-events: none; +} + +.timeline-time-bar.timeline-time-bar-hover { + background-color: var(--light-pink); +} diff --git a/src/cli/traceViewer/web/ui/timeline.tsx b/src/cli/traceViewer/web/ui/timeline.tsx new file mode 100644 index 0000000000..25e48452f5 --- /dev/null +++ b/src/cli/traceViewer/web/ui/timeline.tsx @@ -0,0 +1,156 @@ +/* + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ContextEntry } from '../../traceModel'; +import './timeline.css'; +import { FilmStrip } from './filmStrip'; +import { Boundaries } from '../geometry'; +import * as React from 'react'; +import { useMeasure } from './helpers'; + +export const Timeline: React.FunctionComponent<{ + context: ContextEntry, + boundaries: Boundaries, +}> = ({ context, boundaries }) => { + const [measure, ref] = useMeasure(); + const [previewX, setPreviewX] = React.useState(); + + const offsets = React.useMemo(() => { + return calculateDividerOffsets(measure.width, boundaries); + }, [measure.width, boundaries]); + const actionEntries = React.useMemo(() => { + const actions = []; + for (const page of context.pages) + actions.push(...page.actions); + return actions; + }, [context]); + const actionTimes = React.useMemo(() => { + return actionEntries.map(entry => { + return { + action: entry.action, + actionId: entry.actionId, + left: timeToPercent(measure.width, boundaries, entry.action.startTime!), + right: timeToPercent(measure.width, boundaries, entry.action.endTime!), + }; + }); + }, [actionEntries, boundaries, measure.width]); + + const onMouseMove = (event: React.MouseEvent) => { + if (ref.current) + setPreviewX(event.clientX - ref.current.getBoundingClientRect().left); + }; + const onMouseLeave = () => { + setPreviewX(undefined); + }; + + return
+
{ + offsets.map((offset, index) => { + return
+
{msToString(offset.time - boundaries.minimum)}
+
; + }) + }
+
{ + actionTimes.map(({ action, actionId, left }) => { + return
+ {action.action} +
; + }) + }
+
{ + actionTimes.map(({ action, actionId, left, right }) => { + return
; + }) + }
+ +
+
; +}; + +function calculateDividerOffsets(clientWidth: number, boundaries: Boundaries): { percent: number, time: number }[] { + const minimumGap = 64; + let dividerCount = clientWidth / minimumGap; + const boundarySpan = boundaries.maximum - boundaries.minimum; + const pixelsPerMillisecond = clientWidth / boundarySpan; + let sectionTime = boundarySpan / dividerCount; + + const logSectionTime = Math.ceil(Math.log(sectionTime) / Math.LN10); + sectionTime = Math.pow(10, logSectionTime); + if (sectionTime * pixelsPerMillisecond >= 5 * minimumGap) + sectionTime = sectionTime / 5; + if (sectionTime * pixelsPerMillisecond >= 2 * minimumGap) + sectionTime = sectionTime / 2; + + const firstDividerTime = boundaries.minimum; + let lastDividerTime = boundaries.maximum; + lastDividerTime += minimumGap / pixelsPerMillisecond; + dividerCount = Math.ceil((lastDividerTime - firstDividerTime) / sectionTime); + + if (!sectionTime) + dividerCount = 0; + + const offsets = []; + for (let i = 0; i < dividerCount; ++i) { + const time = firstDividerTime + sectionTime * i; + offsets.push({ percent: timeToPercent(clientWidth, boundaries, time), time }); + } + return offsets; +} + +function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number): number { + const position = (time - boundaries.minimum) / (boundaries.maximum - boundaries.minimum) * clientWidth; + return 100 * position / clientWidth; +} + +function msToString(ms: number): string { + if (!isFinite(ms)) + return '-'; + + if (ms === 0) + return '0'; + + if (ms < 1000) + return ms.toFixed(0) + 'ms'; + + const seconds = ms / 1000; + if (seconds < 60) + return seconds.toFixed(1) + 's'; + + const minutes = seconds / 60; + if (minutes < 60) + return minutes.toFixed(1) + 's'; + + const hours = minutes / 60; + if (hours < 24) + return hours.toFixed(1) + 'h'; + + const days = hours / 24; + return days.toFixed(1) + 'h'; +} diff --git a/src/cli/traceViewer/web/ui/workbench.css b/src/cli/traceViewer/web/ui/workbench.css new file mode 100644 index 0000000000..c27aa7cac8 --- /dev/null +++ b/src/cli/traceViewer/web/ui/workbench.css @@ -0,0 +1,59 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +.workbench { + contain: size; +} + +.workbench .header { + display: flex; + background-color: #000; + flex: none; + flex-basis: 48px; + line-height: 48px; + font-size: 16px; + color: white; +} + +.workbench tab-content { + padding: 25px; + contain: size; +} + +.workbench tab-strip { + margin-left: calc(-1*var(--sidebar-width)); + padding-left: var(--sidebar-width); + box-shadow: var(--box-shadow); +} + +.workbench .logo { + font-size: 20px; + margin-left: 16px; +} + +.workbench .product { + font-weight: 600; + margin-left: 16px; +} + +.workbench .spacer { + flex: auto; +} + +tab-strip { + background-color: var(--light-background); +} + diff --git a/src/cli/traceViewer/web/ui/workbench.tsx b/src/cli/traceViewer/web/ui/workbench.tsx new file mode 100644 index 0000000000..1aeb33be9a --- /dev/null +++ b/src/cli/traceViewer/web/ui/workbench.tsx @@ -0,0 +1,67 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import { ActionEntry, TraceModel } from '../../traceModel'; +import { ActionList } from './actionList'; +import { PropertiesTabbedPane } from './propertiesTabbedPane'; +import { Timeline } from './timeline'; +import './workbench.css'; +import * as React from 'react'; +import { ContextSelector } from './contextSelector'; + +export const Workbench: React.FunctionComponent<{ + traceModel: TraceModel, +}> = ({ traceModel }) => { + const [context, setContext] = React.useState(traceModel.contexts[0]); + const [action, setAction] = React.useState(); + + const actions = React.useMemo(() => { + const actions: ActionEntry[] = []; + for (const page of context.pages) + actions.push(...page.actions); + return actions; + }, [context]); + + const snapshotSize = context.created.viewportSize!; + + return
+
+
🎭
+
Playwright
+
+ { + setContext(context); + setAction(undefined); + }} + /> +
+
+ +
+
+
+ setAction(action)} /> +
+ +
+
; +}; diff --git a/src/cli/traceViewer/web/web.webpack.config.js b/src/cli/traceViewer/web/web.webpack.config.js new file mode 100644 index 0000000000..4fd608469e --- /dev/null +++ b/src/cli/traceViewer/web/web.webpack.config.js @@ -0,0 +1,40 @@ +const path = require('path'); +const HtmlWebPackPlugin = require('html-webpack-plugin'); + +module.exports = { + mode: 'production', + entry: { + app: path.join(__dirname, 'index.tsx'), + }, + resolve: { + extensions: ['.ts', '.js', '.tsx', '.jsx'] + }, + output: { + globalObject: 'self', + filename: '[name].bundle.js', + path: path.resolve(__dirname, '../../../../lib/cli/traceViewer/web') + }, + module: { + rules: [ + { + test: /\.(j|t)sx?$/, + use: 'ts-loader', + exclude: /node_modules/ + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'] + }, + { + test: /\.ttf$/, + use: ['file-loader'] + } + ] + }, + plugins: [ + new HtmlWebPackPlugin({ + title: 'Playwright Trace Viewer', + template: path.join(__dirname, 'index.html'), + }) + ] +}; diff --git a/src/debug/injected/consoleApi.webpack.config.js b/src/debug/injected/consoleApi.webpack.config.js index b15af4b766..68425ec480 100644 --- a/src/debug/injected/consoleApi.webpack.config.js +++ b/src/debug/injected/consoleApi.webpack.config.js @@ -20,6 +20,7 @@ const InlineSource = require('../../server/injected/webpack-inline-source-plugin module.exports = { entry: path.join(__dirname, 'consoleApi.ts'), devtool: 'source-map', + mode: 'development', module: { rules: [ { diff --git a/src/server/injected/injectedScript.webpack.config.js b/src/server/injected/injectedScript.webpack.config.js index 7a0ee7c00e..dc4a08a986 100644 --- a/src/server/injected/injectedScript.webpack.config.js +++ b/src/server/injected/injectedScript.webpack.config.js @@ -20,6 +20,7 @@ const InlineSource = require('./webpack-inline-source-plugin.js'); module.exports = { entry: path.join(__dirname, 'injectedScript.ts'), devtool: 'source-map', + mode: 'development', module: { rules: [ { diff --git a/src/server/injected/utilityScript.webpack.config.js b/src/server/injected/utilityScript.webpack.config.js index 468bbba391..c58f303071 100644 --- a/src/server/injected/utilityScript.webpack.config.js +++ b/src/server/injected/utilityScript.webpack.config.js @@ -20,6 +20,7 @@ const InlineSource = require('./webpack-inline-source-plugin.js'); module.exports = { entry: path.join(__dirname, 'utilityScript.ts'), devtool: 'source-map', + mode: 'development', module: { rules: [ { diff --git a/src/third_party/highlightjs/highlightjs/tomorrow.css b/src/third_party/highlightjs/highlightjs/tomorrow.css new file mode 100644 index 0000000000..026a62fe3b --- /dev/null +++ b/src/third_party/highlightjs/highlightjs/tomorrow.css @@ -0,0 +1,72 @@ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ + +/* Tomorrow Comment */ +.hljs-comment, +.hljs-quote { + color: #8e908c; +} + +/* Tomorrow Red */ +.hljs-variable, +.hljs-template-variable, +.hljs-tag, +.hljs-name, +.hljs-selector-id, +.hljs-selector-class, +.hljs-regexp, +.hljs-deletion { + color: #c82829; +} + +/* Tomorrow Orange */ +.hljs-number, +.hljs-built_in, +.hljs-builtin-name, +.hljs-literal, +.hljs-type, +.hljs-params, +.hljs-meta, +.hljs-link { + color: #f5871f; +} + +/* Tomorrow Yellow */ +.hljs-attribute { + color: #eab700; +} + +/* Tomorrow Green */ +.hljs-string, +.hljs-symbol, +.hljs-bullet, +.hljs-addition { + color: #718c00; +} + +/* Tomorrow Blue */ +.hljs-title, +.hljs-section { + color: #4271ae; +} + +/* Tomorrow Purple */ +.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; +} diff --git a/src/third_party/highlightjs/roll.sh b/src/third_party/highlightjs/roll.sh index e8cceb87d4..856e851bf0 100755 --- a/src/third_party/highlightjs/roll.sh +++ b/src/third_party/highlightjs/roll.sh @@ -6,6 +6,7 @@ set +x # https://github.com/highlightjs/highlight.js/releases RELEASE_REVISION="af20048d5c601d6e30016d8171317bfdf8a6c242" LANGUAGES="javascript python csharp" +STYLES="tomorrow.css" trap "cd $(pwd -P)" EXIT SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)" @@ -27,5 +28,6 @@ mkdir -p ./highlightjs cp -R output/highlight.js/build/lib/* highlightjs/ cp output/highlight.js/build/LICENSE highlightjs/ cp output/highlight.js/build/types/index.d.ts highlightjs/ +cp output/highlight.js/build/styles/${STYLES} highlightjs/ echo $'\n'"export = hljs;"$'\n' >> highlightjs/index.d.ts rm -rf ./output diff --git a/tsconfig.json b/tsconfig.json index 4606fdec4c..d4e4c0f8eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,9 +8,10 @@ "outDir": "./lib", "strict": true, "allowJs": true, - "declaration": false + "declaration": false, + "jsx": "react" }, "compileOnSave": true, "include": ["src/**/*.ts", "src/**/*.js"], - "exclude": ["node_modules", "src/.eslintrc.js"] + "exclude": ["node_modules", "src/.eslintrc.js", "src/cli/traceViewer/web/**"] } diff --git a/utils/build/build.js b/utils/build/build.js index a4b001af78..00902353ab 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -67,11 +67,12 @@ const webPackFiles = [ 'src/server/injected/utilityScript.webpack.config.js', 'src/debug/injected/consoleApi.webpack.config.js', 'src/cli/injected/recorder.webpack.config.js', + 'src/cli/traceViewer/web/web.webpack.config.js', ]; for (const file of webPackFiles) { steps.push({ command: 'npx', - args: ['webpack', '--config', filePath(file), '--mode', 'development', ...(watchMode ? ['--watch', '--silent'] : [])], + args: ['webpack', '--config', filePath(file), ...(watchMode ? ['--watch', '--silent', '--mode', 'development'] : [])], shell: true, }); } diff --git a/utils/check_deps.js b/utils/check_deps.js index 3ceaa043c6..781f7cdbcd 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -150,7 +150,7 @@ DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/ser DEPS['src/service.ts'] = ['src/remote/']; // CLI should only use client-side features. -DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/']; +DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**']; checkDeps().catch(e => { console.error(e && e.stack ? e.stack : e);