From 351994972d626a8bed53ab5a1bed095d5f59909f Mon Sep 17 00:00:00 2001 From: Jamie Wong Date: Sat, 23 May 2020 16:42:31 -0700 Subject: [PATCH] Upgrade to Preact X, partially convert to using hooks (#267) I'd like to try writing new components using hooks, and to do that I need to upgrade from preact 8 to preact X. For reasons that are... complicated, in order to upgrade without breaking part of my build process, I had to remove the dependency on `preact-redux` altogether. This led me to write my own implementation, and as part of that I realized I could remove `createContainer` in favour of some simple hooks that use redux. Before landing: - [x] Investigate performance issues in the sandwich views - [x] Investigate es-lint checks for exhaustive hook dependencies --- .eslintrc.js | 10 +- package-lock.json | 957 +++++++++++------- package.json | 17 +- src/lib/preact-redux.tsx | 57 ++ src/lib/typed-redux.ts | 28 +- src/speedscope.tsx | 6 +- src/store/actions.ts | 2 +- src/store/index.ts | 9 +- src/store/store-test-utils.ts | 4 +- src/typings/preact-redux.d.ts | 12 - src/views/application-container.tsx | 99 +- src/views/application.tsx | 238 +---- src/views/callee-flamegraph-view.ts | 84 -- src/views/callee-flamegraph-view.tsx | 87 ++ src/views/color-chit.tsx | 8 +- src/views/flamechart-detail-view.tsx | 2 +- src/views/flamechart-minimap-view.tsx | 4 +- src/views/flamechart-pan-zoom-view.tsx | 4 +- src/views/flamechart-view-container.tsx | 157 +-- src/views/flamechart-view.tsx | 2 +- src/views/flamechart-wrapper.tsx | 2 +- src/views/hovertip.tsx | 7 +- src/views/inverted-caller-flamegraph-view.ts | 96 -- src/views/inverted-caller-flamegraph-view.tsx | 99 ++ src/views/profile-table-view.tsx | 404 ++++---- src/views/sandwich-view.tsx | 46 +- src/views/scrollable-list-view.tsx | 4 +- src/views/toolbar.tsx | 230 +++++ 28 files changed, 1449 insertions(+), 1226 deletions(-) create mode 100644 src/lib/preact-redux.tsx delete mode 100644 src/typings/preact-redux.d.ts delete mode 100644 src/views/callee-flamegraph-view.ts create mode 100644 src/views/callee-flamegraph-view.tsx delete mode 100644 src/views/inverted-caller-flamegraph-view.ts create mode 100644 src/views/inverted-caller-flamegraph-view.tsx create mode 100644 src/views/toolbar.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 3fc226ae9..411b33636 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,13 +1,15 @@ module.exports = { - parser: 'typescript-eslint-parser', + parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', ecmaFeatures: { jsx: true, }, }, - plugins: ['prettier'], + plugins: ['prettier', '@typescript-eslint', 'react-hooks'], rules: { - 'prettier/prettier': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', }, -}; +} diff --git a/package-lock.json b/package-lock.json index 57283432b..192a15f57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1703,6 +1703,12 @@ "@babel/types": "^7.3.0" } }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1794,6 +1800,94 @@ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.33.0.tgz", + "integrity": "sha512-QV6P32Btu1sCI/kTqjTNI/8OpCYyvlGjW5vD8MpTIg+HGE5S88HtT1G+880M4bXlvXj/NjsJJG0aGcVh0DdbeQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.33.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.33.0.tgz", + "integrity": "sha512-qzPM2AuxtMrRq78LwyZa8Qn6gcY8obkIrBs1ehqmQADwkYzTE1Pb4y2W+U3rE/iFkSWcWHG2LS6MJfj6SmHApg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.33.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.33.0.tgz", + "integrity": "sha512-AUtmwUUhJoH6yrtxZMHbRUEMsC2G6z5NSxg9KsROOGqNXasM71I8P2NihtumlWTUCRld70vqIZ6Pm4E5PAziEA==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.33.0", + "@typescript-eslint/typescript-estree": "2.33.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.33.0.tgz", + "integrity": "sha512-d8rY6/yUxb0+mEwTShCQF2zYQdLlqihukNfG9IUlLYz5y1CH6G/9XYbrxQLq3Z14RNvkCC6oe+OcFlyUpwUbkg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "abab": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", @@ -1801,9 +1895,9 @@ "dev": true }, "acorn": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.2.1.tgz", - "integrity": "sha512-jG0u7c4Ly+3QkkW18V+NRDN+4bWHdln30NL1ZL2AvFZZmQe/BfopYCtghCKKVBUSetZ4QKcyA0pY6/4Gw8Pv8w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", "dev": true }, "acorn-globals": { @@ -1825,21 +1919,10 @@ } }, "acorn-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", - "dev": true, - "requires": { - "acorn": "^3.0.4" - }, - "dependencies": { - "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", - "dev": true - } - } + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true }, "acorn-walk": { "version": "6.2.0", @@ -1859,12 +1942,6 @@ "json-schema-traverse": "^0.3.0" } }, - "ajv-keywords": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", - "dev": true - }, "alphanum-sort": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", @@ -2222,33 +2299,12 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -2353,41 +2409,6 @@ "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", "dev": true }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - }, - "dependencies": { - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "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" - } - } - } - }, "babel-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", @@ -2900,19 +2921,10 @@ } } }, - "caller-path": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", - "dev": true, - "requires": { - "callsites": "^0.2.0" - } - }, "callsites": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, "camelcase": { @@ -2986,9 +2998,9 @@ } }, "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, "chokidar": { @@ -3035,12 +3047,6 @@ "safe-buffer": "^5.0.1" } }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -3143,9 +3149,9 @@ "dev": true }, "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, "cliui": { @@ -3895,21 +3901,6 @@ } } }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3962,9 +3953,9 @@ } }, "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "requires": { "esutils": "^2.0.2" @@ -4207,86 +4198,128 @@ } }, "eslint": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz", - "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.0.0.tgz", + "integrity": "sha512-SrrIfcd4tOgsspOKTSwamuTOAMZOUigHQhVMrzNjz4/B9Za6SHQDIocMIyIDfwDgx6MhS15nS6HC8kumCV2qBQ==", "dev": true, "requires": { - "ajv": "^5.3.0", - "babel-code-frame": "^6.22.0", + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", "chalk": "^2.1.0", - "concat-stream": "^1.6.0", - "cross-spawn": "^5.1.0", - "debug": "^3.1.0", - "doctrine": "^2.1.0", - "eslint-scope": "^3.7.1", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^4.0.3", + "eslint-utils": "^1.3.1", "eslint-visitor-keys": "^1.0.0", - "espree": "^3.5.4", - "esquery": "^1.0.0", + "espree": "^6.0.0", + "esquery": "^1.0.1", "esutils": "^2.0.2", - "file-entry-cache": "^2.0.0", + "file-entry-cache": "^5.0.1", "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.0.1", - "ignore": "^3.3.3", + "glob-parent": "^3.1.0", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^3.0.6", - "is-resolvable": "^1.0.0", - "js-yaml": "^3.9.1", + "inquirer": "^6.2.2", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.3.0", - "lodash": "^4.17.4", - "minimatch": "^3.0.2", + "lodash": "^4.17.11", + "minimatch": "^3.0.4", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "pluralize": "^7.0.0", "progress": "^2.0.0", - "regexpp": "^1.0.1", - "require-uncached": "^1.0.3", - "semver": "^5.3.0", + "regexpp": "^2.0.1", + "semver": "^5.5.1", "strip-ansi": "^4.0.0", - "strip-json-comments": "~2.0.1", - "table": "4.0.2", - "text-table": "~0.2.0" + "strip-json-comments": "^2.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0" }, "dependencies": { - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", "dev": true, "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "dev": true, "requires": { - "ms": "2.0.0" + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" } }, - "globals": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.4.0.tgz", - "integrity": "sha512-Dyzmifil8n/TmSqYDEXbm+C8yitzJQqQIlJQLNRMwa+BOUJpRC19pyVeN12JAjt61xonvXjtff+hJruTRXn5HA==", + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", "dev": true }, - "lru-cache": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", - "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", "dev": true, "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true } } }, @@ -4300,38 +4333,46 @@ "jest-docblock": "^21.0.0" } }, + "eslint-plugin-react-hooks": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.2.tgz", + "integrity": "sha512-kAMRjNztrLW1rK+81X1NwMB2LqG+nc7Q8AibnG8/VyWhQK8SP6JotCFG+HL4u1EjziplxVz4jARdR8gGk8pLDA==", + "dev": true + }, "eslint-scope": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", - "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", "dev": true, "requires": { "esrecurse": "^4.1.0", "estraverse": "^4.1.1" } }, + "eslint-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz", + "integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", "dev": true }, "espree": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz", - "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "acorn": "^5.5.0", - "acorn-jsx": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", - "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", - "dev": true - } + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" } }, "esprima": { @@ -4341,12 +4382,20 @@ "dev": true }, "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "estraverse": "^4.0.0" + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", + "dev": true + } } }, "esrecurse": { @@ -4564,13 +4613,13 @@ } }, "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", "dev": true, "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, @@ -4628,6 +4677,12 @@ "object-keys": "^1.0.6" }, "dependencies": { + "acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -4699,13 +4754,12 @@ } }, "file-entry-cache": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", "dev": true, "requires": { - "flat-cache": "^1.2.1", - "object-assign": "^4.0.1" + "flat-cache": "^2.0.1" } }, "file-uri-to-path": { @@ -4753,17 +4807,47 @@ } }, "flat-cache": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", - "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", "dev": true, "requires": { - "circular-json": "^0.3.1", - "del": "^2.0.2", - "graceful-fs": "^4.1.2", - "write": "^0.2.1" + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -5544,20 +5628,6 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, - "globby": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -5723,6 +5793,15 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + } + }, "hosted-git-info": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", @@ -5879,12 +5958,12 @@ "dev": true }, "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "icss-replace-symbols": { @@ -5900,9 +5979,9 @@ "dev": true }, "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, "immediate": { @@ -5977,25 +6056,67 @@ "dev": true }, "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", + "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", - "external-editor": "^2.0.4", + "external-editor": "^3.0.3", "figures": "^2.0.0", - "lodash": "^4.3.0", + "lodash": "^4.17.12", "mute-stream": "0.0.7", "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", + "rxjs": "^6.4.0", "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", + "strip-ansi": "^5.1.0", "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "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" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } } }, "invariant": { @@ -6202,30 +6323,6 @@ } } }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -6243,12 +6340,6 @@ } } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -6985,8 +7076,7 @@ "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" }, "js-yaml": { "version": "3.13.1", @@ -7330,7 +7420,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", - "dev": true, "requires": { "js-tokens": "^3.0.0" } @@ -7796,8 +7885,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -8225,6 +8313,15 @@ } } }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-asn1": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", @@ -8291,12 +8388,6 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -8351,27 +8442,6 @@ "integrity": "sha1-GN4vl+S/epVRrXURlCtUlverpmA=", "dev": true }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, "pirates": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", @@ -8444,12 +8514,6 @@ } } }, - "pluralize": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", - "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", - "dev": true - }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -9100,14 +9164,9 @@ "dev": true }, "preact": { - "version": "8.2.7", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.2.7.tgz", - "integrity": "sha512-m34Ke8U32HyKRVzUOCAcaiIBLR2ye6syiuRclU5DxyixDPDFqdLbIElhERBrF6gDbPKQR+Vpv5bZ9CCbvN6pdQ==", - "dev": true - }, - "preact-redux": { - "version": "github:jlfwong/preact-redux#a56dcc460f4993c8dd33b7c9caa5e4bde1fa72dd", - "from": "github:jlfwong/preact-redux#a56dcc4", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.4.1.tgz", + "integrity": "sha512-WKrRpCSwL2t3tpOOGhf2WfTpcmbpxaWtDbdJdKdjd0aEiTkvOmS4NBkG6kzlaAHI9AkQ3iVqbFWM3Ei7mZ4o1Q==", "dev": true }, "prelude-ls": { @@ -9170,9 +9229,9 @@ "dev": true }, "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, "prompts": { @@ -9185,6 +9244,26 @@ "sisteransi": "^1.0.3" } }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + }, + "dependencies": { + "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==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + } + } + }, "protobufjs": { "version": "6.8.8", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", @@ -9214,12 +9293,6 @@ } } }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, "psl": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", @@ -9439,11 +9512,44 @@ "integrity": "sha1-CMbgSgFo9utiHCKrbLEVG9n0pk0=", "dev": true }, + "react": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", + "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + } + }, "react-is": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz", - "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==", - "dev": true + "integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==" + }, + "react-redux": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz", + "integrity": "sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + }, + "dependencies": { + "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" + } + } + } }, "read-pkg": { "version": "3.0.0", @@ -9502,13 +9608,24 @@ } }, "redux": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.0.tgz", - "integrity": "sha512-NnnHF0h0WVE/hXyrB6OlX67LYRuaf/rJcbWvnHHEPCF/Xa/AZpwhs/20WyqzQae5x4SD2F9nPObgBh2rxAgLiA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", "dev": true, "requires": { - "loose-envify": "^1.1.0", + "loose-envify": "^1.4.0", "symbol-observable": "^1.2.0" + }, + "dependencies": { + "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" + } + } } }, "regenerate": { @@ -9564,9 +9681,9 @@ } }, "regexpp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz", - "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "regexpu-core": { @@ -9686,16 +9803,6 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, - "require-uncached": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", - "dev": true, - "requires": { - "caller-path": "^0.1.0", - "resolve-from": "^1.0.0" - } - }, "resolve": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", @@ -9723,9 +9830,9 @@ } }, "resolve-from": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, "resolve-url": { @@ -9788,27 +9895,18 @@ "dev": true }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", "dev": true, "requires": { - "rx-lite": "*" + "tslib": "^1.9.0" } }, "safe-buffer": { @@ -10033,12 +10131,25 @@ "dev": true }, "slice-ansi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", - "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", "dev": true, "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + } } }, "snapdragon": { @@ -10927,10 +11038,13 @@ } }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "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" + } }, "svgo": { "version": "1.3.2", @@ -10997,17 +11111,67 @@ "dev": true }, "table": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", - "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", "dev": true, "requires": { - "ajv": "^5.2.3", - "ajv-keywords": "^2.1.0", - "chalk": "^2.1.0", - "lodash": "^4.17.4", - "slice-ansi": "1.0.0", - "string-width": "^2.1.1" + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } } }, "terser": { @@ -11261,6 +11425,21 @@ } } }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", @@ -12052,9 +12231,9 @@ "dev": true }, "write": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", "dev": true, "requires": { "mkdirp": "^0.5.1" @@ -12098,12 +12277,6 @@ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", "dev": true }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, "yargs": { "version": "13.3.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", diff --git a/package.json b/package.json index f8301e52f..f7c134319 100644 --- a/package.json +++ b/package.json @@ -34,24 +34,28 @@ "@types/jszip": "3.1.4", "@types/node": "14.0.1", "@types/pako": "1.0.0", + "@typescript-eslint/eslint-plugin": "2.33.0", + "@typescript-eslint/parser": "2.33.0", + "acorn": "7.2.0", "aphrodite": "2.1.0", "coveralls": "3.0.1", - "eslint": "4.19.1", + "eslint": "6.0.0", "eslint-plugin-prettier": "2.6.0", + "eslint-plugin-react-hooks": "4.0.2", "jest": "24.3.0", "jsverify": "0.8.3", "jszip": "3.1.5", "pako": "1.0.6", "parcel-bundler": "1.12.4", - "preact": "8.2.7", - "preact-redux": "jlfwong/preact-redux#a56dcc4", + "preact": "10.4.1", "prettier": "2.0.4", "protobufjs": "6.8.8", - "typescript-json-schema": "0.42.0", - "redux": "4.0.0", + "react-redux": "^7.2.0", + "redux": "^4.0.5", "ts-jest": "24.3.0", "typescript": "3.9.2", "typescript-eslint-parser": "17.0.1", + "typescript-json-schema": "0.42.0", "uglify-es": "3.2.2" }, "jest": { @@ -75,6 +79,7 @@ ] }, "dependencies": { - "opn": "5.3.0" + "opn": "5.3.0", + "react": "^16.13.1" } } diff --git a/src/lib/preact-redux.tsx b/src/lib/preact-redux.tsx new file mode 100644 index 000000000..c9762ed0b --- /dev/null +++ b/src/lib/preact-redux.tsx @@ -0,0 +1,57 @@ +/** + * As of Preact 10.x, they no longer have an officially supported preact-redux library. + * It's possible to use react-redux with some hacks, but these hacks cause npm run pack + * to error out because of (intentinoally) unmet peer dependencies. + * + * I could stack more hacks to fix this problem, but I'd rather just drop the dependency + * and remove the need to do any dependency hacking by writing the very small part of + * react-redux that I actually need myself. + */ + +import {h} from 'preact' +import * as redux from 'redux' +import {createContext, ComponentChildren} from 'preact' +import {Dispatch, Action} from './typed-redux' +import {useState, useContext, useCallback, useLayoutEffect} from 'preact/hooks' + +const PreactRedux = createContext | null>(null) + +interface ProviderProps { + store: redux.Store + children?: ComponentChildren +} + +export function Provider(props: ProviderProps) { + return +} + +function useStore(): redux.Store { + const store = useContext(PreactRedux) + if (store == null) { + throw new Error('Called useStore when no store exists in context') + } + return store +} + +export function useDispatch(): Dispatch { + const store = useStore() + return store.dispatch +} + +export function useActionCreator(creator: (payload: T) => Action): (t: T) => void { + const dispatch = useDispatch() + return useCallback((t: T) => dispatch(creator(t)), [dispatch, creator]) +} + +export function useSelector(selector: (t: T) => U): U { + const store = useStore() + const [value, setValue] = useState(() => selector(store.getState())) + + useLayoutEffect(() => { + return store.subscribe(() => { + setValue(selector(store.getState())) + }) + }, [store, selector]) + + return value +} diff --git a/src/lib/typed-redux.ts b/src/lib/typed-redux.ts index d699ff989..038003b46 100644 --- a/src/lib/typed-redux.ts +++ b/src/lib/typed-redux.ts @@ -1,6 +1,5 @@ -import {connect} from 'preact-redux' import * as redux from 'redux' -import {ComponentConstructor, Component} from 'preact' +import {Component} from 'preact' export interface Action extends redux.Action { payload: TPayload @@ -53,31 +52,6 @@ export function setter( export type Dispatch = redux.Dispatch> -// We make this into a single function invocation instead of the connect(map, map)(Component) -// syntax to make better use of type inference. -// -// NOTE: To avoid this returning objects which do not compare shallow equal, it's the -// responsibility of the caller to ensure that the props returned by map compare shallow -// equal. This most importantly mean memoizing functions which wrap dispatch to avoid -// all callback props from being regenerated on every call. -export function createContainer( - component: { - new (props: ComponentProps): ComponentType - }, - map: (state: State, dispatch: Dispatch, ownProps: OwnProps) => ComponentProps, -): ComponentConstructor { - const mapStateToProps = (state: State) => state - const mapDispatchToProps = (dispatch: Dispatch) => ({dispatch}) - const mergeProps = ( - stateProps: State, - dispatchProps: {dispatch: Dispatch}, - ownProps: OwnProps, - ) => { - return map(stateProps, dispatchProps.dispatch, ownProps) - } - return connect(mapStateToProps, mapDispatchToProps, mergeProps)(component) -} - export type VoidState = { __dummyField: void } diff --git a/src/speedscope.tsx b/src/speedscope.tsx index 2b3d61578..fb92714c8 100644 --- a/src/speedscope.tsx +++ b/src/speedscope.tsx @@ -1,7 +1,7 @@ import {h, render} from 'preact' -import {createApplicationStore} from './store' -import {Provider} from 'preact-redux' +import {createAppStore} from './store' import {ApplicationContainer} from './views/application-container' +import {Provider} from './lib/preact-redux' console.log(`speedscope v${require('../package.json').version}`) @@ -15,7 +15,7 @@ if (module.hot) { } const lastStore: any = (window as any)['store'] -const store = createApplicationStore(lastStore ? lastStore.getState() : {}) +const store = lastStore ? createAppStore(lastStore.getState()) : createAppStore() ;(window as any)['store'] = store render( diff --git a/src/store/actions.ts b/src/store/actions.ts index 0a655b50b..2b379df5a 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -41,7 +41,7 @@ export namespace actions { export const setTableSortMethod = actionCreator('sandwichView.setTableSortMethod') export const setSelectedFrame = actionCreatorWithIndex( - 'sandwichView.setSelectedFarmr', + 'sandwichView.setSelectedFrame', ) } diff --git a/src/store/index.ts b/src/store/index.ts index 58854eb28..8528855d1 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -10,6 +10,7 @@ import {setter, Reducer} from '../lib/typed-redux' import {HashParams, getHashParams} from '../lib/hash-params' import {ProfileGroupState, profileGroup} from './profiles-state' import {SortMethod, SortField, SortDirection} from '../views/profile-table-view' +import {useSelector} from '../lib/preact-redux' export const enum ViewMode { CHRONO_FLAME_CHART, @@ -41,9 +42,7 @@ const protocol = window.location.protocol // however, XHR will be unavailable to fetching files in adjacent directories. export const canUseXHR = protocol === 'http:' || protocol === 'https:' -export function createApplicationStore( - initialState: Partial, -): redux.Store { +export function createAppStore(initialState?: ApplicationState): redux.Store { const hashParams = getHashParams() const loading = canUseXHR && hashParams.profileURL != null @@ -71,3 +70,7 @@ export function createApplicationStore( return redux.createStore(reducer, initialState) } + +export function useAppSelector(selector: (t: ApplicationState) => T): T { + return useSelector(selector) +} diff --git a/src/store/store-test-utils.ts b/src/store/store-test-utils.ts index cfa268e0b..ff5b8e0bf 100644 --- a/src/store/store-test-utils.ts +++ b/src/store/store-test-utils.ts @@ -1,11 +1,11 @@ import * as fs from 'fs' import {Store, AnyAction} from 'redux' -import {ApplicationState, createApplicationStore} from '.' +import {ApplicationState, createAppStore} from '.' import {importSpeedscopeProfiles} from '../lib/file-format' export function storeTest(name: string, cb: (store: Store) => void) { - const store = createApplicationStore({}) + const store = createAppStore() test(name, () => { cb(store) }) diff --git a/src/typings/preact-redux.d.ts b/src/typings/preact-redux.d.ts deleted file mode 100644 index dda0f14a9..000000000 --- a/src/typings/preact-redux.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare module 'preact-redux' { - import {VNode, Component} from 'preact' - import {Store} from 'redux' - - // We just export the bare minimum here because we're going - // to implement an API for readability convenience elsewhere - export function connect(...args: any[]): any - - export class Provider extends Component<{store: Store}, {}> { - render(): VNode - } -} diff --git a/src/views/application-container.tsx b/src/views/application-container.tsx index 5c76250f4..45dbee652 100644 --- a/src/views/application-container.tsx +++ b/src/views/application-container.tsx @@ -1,63 +1,50 @@ -import {createContainer, Dispatch, bindActionCreator, ActionCreator} from '../lib/typed-redux' +import {h} from 'preact' import {Application, ActiveProfileState} from './application' -import {ApplicationState} from '../store' import {getProfileToView, getCanvasContext} from '../store/getters' import {actions} from '../store/actions' -import {Graphics} from '../gl/graphics' +import {useActionCreator} from '../lib/preact-redux' +import {memo} from 'preact/compat' +import {useCallback} from 'preact/hooks' +import {useAppSelector} from '../store' -export const ApplicationContainer = createContainer( - Application, - (state: ApplicationState, dispatch: Dispatch) => { - const {flattenRecursion, profileGroup} = state +export const ApplicationContainer = memo(() => { + const appState = useAppSelector(useCallback(state => state, [])) + const canvasContext = useAppSelector( + useCallback(state => (state.glCanvas ? getCanvasContext(state.glCanvas) : null), []), + ) - let activeProfileState: ActiveProfileState | null = null - if (profileGroup) { - if (profileGroup.profiles.length > profileGroup.indexToView) { - const index = profileGroup.indexToView - const profileState = profileGroup.profiles[index] - activeProfileState = { - ...profileGroup.profiles[profileGroup.indexToView], - profile: getProfileToView({profile: profileState.profile, flattenRecursion}), - index: profileGroup.indexToView, - } - } - } - - function wrapActionCreator(actionCreator: ActionCreator): (t: T) => void { - return bindActionCreator(dispatch, actionCreator) - } + const activeProfileState: ActiveProfileState | null = useAppSelector( + useCallback(state => { + const {profileGroup} = state + if (!profileGroup) return null + if (profileGroup.indexToView >= profileGroup.profiles.length) return null - // TODO(jlfwong): Cache this and resizeCanvas below to prevent re-renders - // due to changing props. - const setters = { - setGLCanvas: wrapActionCreator(actions.setGLCanvas), - setLoading: wrapActionCreator(actions.setLoading), - setError: wrapActionCreator(actions.setError), - setProfileGroup: wrapActionCreator(actions.setProfileGroup), - setDragActive: wrapActionCreator(actions.setDragActive), - setViewMode: wrapActionCreator(actions.setViewMode), - setFlattenRecursion: wrapActionCreator(actions.setFlattenRecursion), - setProfileIndexToView: wrapActionCreator(actions.setProfileIndexToView), - } + const index = profileGroup.indexToView + const profileState = profileGroup.profiles[index] + return { + ...profileGroup.profiles[profileGroup.indexToView], + profile: getProfileToView({ + profile: profileState.profile, + flattenRecursion: state.flattenRecursion, + }), + index: profileGroup.indexToView, + } + }, []), + ) - return { - activeProfileState, - dispatch, - canvasContext: state.glCanvas ? getCanvasContext(state.glCanvas) : null, - resizeCanvas: ( - widthInPixels: number, - heightInPixels: number, - widthInAppUnits: number, - heightInAppUnits: number, - ) => { - if (state.glCanvas) { - const gl = getCanvasContext(state.glCanvas).gl - gl.resize(widthInPixels, heightInPixels, widthInAppUnits, heightInAppUnits) - gl.clear(new Graphics.Color(1, 1, 1, 1)) - } - }, - ...setters, - ...state, - } - }, -) + return ( + + ) +}) diff --git a/src/views/application.tsx b/src/views/application.tsx index 602ce18b0..31482fb54 100644 --- a/src/views/application.tsx +++ b/src/views/application.tsx @@ -1,9 +1,9 @@ -import {h, Component} from 'preact' +import {h} from 'preact' import {StyleSheet, css} from 'aphrodite' import {FileSystemDirectoryEntry} from '../import/file-system-entry' import {Profile, ProfileGroup} from '../lib/profile' -import {FontFamily, FontSize, Colors, Sizes, Duration} from './style' +import {FontFamily, FontSize, Colors, Duration} from './style' import {importEmscriptenSymbolMap} from '../lib/emscripten' import {SandwichViewContainer} from './sandwich-view' import {saveToFile} from '../lib/file-format' @@ -14,6 +14,7 @@ import {SandwichViewState} from '../store/sandwich-view-state' import {FlamechartViewState} from '../store/flamechart-view-state' import {CanvasContext} from '../gl/canvas-context' import {Graphics} from '../gl/graphics' +import {Toolbar} from './toolbar' const importModule = import('../import') // Force eager loading of the module @@ -50,155 +51,14 @@ async function importFromFileSystemDirectoryEntry(entry: FileSystemDirectoryEntr declare function require(x: string): any const exampleProfileURL = require('../../sample/profiles/stackcollapse/perf-vertx-stacks-01-collapsed-all.txt') -interface ToolbarProps extends ApplicationProps { - browseForFile(): void - saveFile(): void -} - -export class Toolbar extends StatelessComponent { - setTimeOrder = () => { - this.props.setViewMode(ViewMode.CHRONO_FLAME_CHART) - } - - setLeftHeavyOrder = () => { - this.props.setViewMode(ViewMode.LEFT_HEAVY_FLAME_GRAPH) - } - - setSandwichView = () => { - this.props.setViewMode(ViewMode.SANDWICH_VIEW) - } - - renderLeftContent() { - if (!this.props.activeProfileState) return null - - return ( -
-
- 🕰Time Order -
-
- ⬅️Left Heavy -
-
- 🥪Sandwich -
-
- ) - } - - renderCenterContent() { - const {activeProfileState, profileGroup} = this.props - if (activeProfileState && profileGroup) { - const {index} = activeProfileState - if (profileGroup.profiles.length === 1) { - return activeProfileState.profile.getName() - } else { - function makeNavButton(content: string, disabled: boolean, onClick: () => void) { - return ( - - ) - } - - const prevButton = makeNavButton('⬅️', index === 0, () => - this.props.setProfileIndexToView(index - 1), - ) - const nextButton = makeNavButton('➡️', index >= profileGroup.profiles.length - 1, () => - this.props.setProfileIndexToView(index + 1), - ) - - return ( -
- {prevButton} - {activeProfileState.profile.getName()}{' '} - - ({activeProfileState.index + 1}/{profileGroup.profiles.length}) - - {nextButton} -
- ) - } - } - return '🔬speedscope' - } - - renderRightContent() { - const importFile = ( -
- ⤵️Import -
- ) - const help = ( - - ) - - return ( -
- {this.props.activeProfileState && ( -
- ⤴️Export -
- )} - {importFile} - {help} -
- ) - } - - render() { - return ( -
- {this.renderLeftContent()} - {this.renderCenterContent()} - {this.renderRightContent()} -
- ) - } -} - interface GLCanvasProps { canvasContext: CanvasContext | null setGLCanvas: (canvas: HTMLCanvasElement | null) => void } -export class GLCanvas extends Component { +export class GLCanvas extends StatelessComponent { private canvas: HTMLCanvasElement | null = null - private ref = (canvas?: Element) => { + private ref = (canvas: Element | null) => { if (canvas instanceof HTMLCanvasElement) { this.canvas = canvas } else { @@ -209,7 +69,7 @@ export class GLCanvas extends Component { } private container: HTMLElement | null = null - private containerRef = (container?: Element) => { + private containerRef = (container: Element | null) => { if (container instanceof HTMLElement) { this.container = container } else { @@ -790,90 +650,4 @@ const style = StyleSheet.create({ cursor: 'pointer', textDecoration: 'none', }, - toolbar: { - height: Sizes.TOOLBAR_HEIGHT, - flexShrink: 0, - background: Colors.BLACK, - color: Colors.WHITE, - textAlign: 'center', - fontFamily: FontFamily.MONOSPACE, - fontSize: FontSize.TITLE, - lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`, - userSelect: 'none', - }, - toolbarLeft: { - position: 'absolute', - height: Sizes.TOOLBAR_HEIGHT, - overflow: 'hidden', - top: 0, - left: 0, - marginRight: 2, - textAlign: 'left', - }, - toolbarCenter: { - paddingTop: 1, - height: Sizes.TOOLBAR_HEIGHT, - }, - toolbarRight: { - height: Sizes.TOOLBAR_HEIGHT, - overflow: 'hidden', - position: 'absolute', - top: 0, - right: 0, - marginRight: 2, - textAlign: 'right', - }, - toolbarProfileIndex: { - color: Colors.LIGHT_GRAY, - }, - toolbarProfileNavButton: { - opacity: 0.8, - fontSize: FontSize.TITLE, - lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`, - ':hover': { - opacity: 1.0, - }, - background: 'none', - border: 'none', - padding: 0, - marginLeft: '0.3em', - marginRight: '0.3em', - transition: `all ${Duration.HOVER_CHANGE} ease-in`, - }, - toolbarProfileNavButtonDisabled: { - opacity: 0.5, - ':hover': { - opacity: 0.5, - }, - }, - toolbarTab: { - background: Colors.DARK_GRAY, - marginTop: Sizes.SEPARATOR_HEIGHT, - height: Sizes.TOOLBAR_TAB_HEIGHT, - lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`, - paddingLeft: 2, - paddingRight: 8, - display: 'inline-block', - marginLeft: 2, - transition: `all ${Duration.HOVER_CHANGE} ease-in`, - ':hover': { - background: Colors.GRAY, - }, - }, - toolbarTabActive: { - background: Colors.BRIGHT_BLUE, - ':hover': { - background: Colors.BRIGHT_BLUE, - }, - }, - noLinkStyle: { - textDecoration: 'none', - color: 'inherit', - }, - emoji: { - display: 'inline-block', - verticalAlign: 'middle', - paddingTop: '0px', - marginRight: '0.3em', - }, }) diff --git a/src/views/callee-flamegraph-view.ts b/src/views/callee-flamegraph-view.ts deleted file mode 100644 index 595823d19..000000000 --- a/src/views/callee-flamegraph-view.ts +++ /dev/null @@ -1,84 +0,0 @@ -import {memoizeByShallowEquality} from '../lib/utils' -import {Profile, Frame} from '../lib/profile' -import {Flamechart} from '../lib/flamechart' -import { - createMemoizedFlamechartRenderer, - FlamechartViewContainerProps, - createFlamechartSetters, -} from './flamechart-view-container' -import {createContainer, Dispatch} from '../lib/typed-redux' -import {ApplicationState} from '../store' -import { - getCanvasContext, - createGetColorBucketForFrame, - createGetCSSColorForFrame, - getFrameToColorBucket, -} from '../store/getters' -import {FlamechartID} from '../store/flamechart-view-state' -import {FlamechartWrapper} from './flamechart-wrapper' - -const getCalleeProfile = memoizeByShallowEquality< - { - profile: Profile - frame: Frame - flattenRecursion: boolean - }, - Profile ->(({profile, frame, flattenRecursion}) => { - let p = profile.getProfileForCalleesOf(frame) - return flattenRecursion ? p.getProfileWithRecursionFlattened() : p -}) - -const getCalleeFlamegraph = memoizeByShallowEquality< - { - calleeProfile: Profile - getColorBucketForFrame: (frame: Frame) => number - }, - Flamechart ->(({calleeProfile, getColorBucketForFrame}) => { - return new Flamechart({ - getTotalWeight: calleeProfile.getTotalNonIdleWeight.bind(calleeProfile), - forEachCall: calleeProfile.forEachCallGrouped.bind(calleeProfile), - formatValue: calleeProfile.formatValue.bind(calleeProfile), - getColorBucketForFrame, - }) -}) - -const getCalleeFlamegraphRenderer = createMemoizedFlamechartRenderer() - -export const CalleeFlamegraphView = createContainer( - FlamechartWrapper, - (state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => { - const {activeProfileState} = ownProps - const {index, profile, sandwichViewState} = activeProfileState - const {flattenRecursion, glCanvas} = state - if (!profile) throw new Error('profile missing') - if (!glCanvas) throw new Error('glCanvas missing') - const {callerCallee} = sandwichViewState - if (!callerCallee) throw new Error('callerCallee missing') - const {selectedFrame} = callerCallee - - const frameToColorBucket = getFrameToColorBucket(profile) - const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) - const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) - const canvasContext = getCanvasContext(glCanvas) - - const flamechart = getCalleeFlamegraph({ - calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}), - getColorBucketForFrame, - }) - const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart}) - - return { - renderInverted: false, - flamechart, - flamechartRenderer, - canvasContext, - getCSSColorForFrame, - ...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_CALLEES, index), - // This overrides the setSelectedNode specified in createFlamechartSettesr - setSelectedNode: () => {}, - ...callerCallee.calleeFlamegraph, - } - }, -) diff --git a/src/views/callee-flamegraph-view.tsx b/src/views/callee-flamegraph-view.tsx new file mode 100644 index 000000000..c640f2b56 --- /dev/null +++ b/src/views/callee-flamegraph-view.tsx @@ -0,0 +1,87 @@ +import {memoizeByShallowEquality, noop} from '../lib/utils' +import {Profile, Frame} from '../lib/profile' +import {Flamechart} from '../lib/flamechart' +import { + createMemoizedFlamechartRenderer, + FlamechartViewContainerProps, + useFlamechartSetters, +} from './flamechart-view-container' +import { + getCanvasContext, + createGetColorBucketForFrame, + createGetCSSColorForFrame, + getFrameToColorBucket, +} from '../store/getters' +import {FlamechartID} from '../store/flamechart-view-state' +import {FlamechartWrapper} from './flamechart-wrapper' +import {useAppSelector} from '../store' +import {h} from 'preact' +import {memo} from 'preact/compat' +import {useCallback} from 'preact/hooks' + +const getCalleeProfile = memoizeByShallowEquality< + { + profile: Profile + frame: Frame + flattenRecursion: boolean + }, + Profile +>(({profile, frame, flattenRecursion}) => { + let p = profile.getProfileForCalleesOf(frame) + return flattenRecursion ? p.getProfileWithRecursionFlattened() : p +}) + +const getCalleeFlamegraph = memoizeByShallowEquality< + { + calleeProfile: Profile + getColorBucketForFrame: (frame: Frame) => number + }, + Flamechart +>(({calleeProfile, getColorBucketForFrame}) => { + return new Flamechart({ + getTotalWeight: calleeProfile.getTotalNonIdleWeight.bind(calleeProfile), + forEachCall: calleeProfile.forEachCallGrouped.bind(calleeProfile), + formatValue: calleeProfile.formatValue.bind(calleeProfile), + getColorBucketForFrame, + }) +}) + +const getCalleeFlamegraphRenderer = createMemoizedFlamechartRenderer() + +export const CalleeFlamegraphView = memo((ownProps: FlamechartViewContainerProps) => { + const {activeProfileState} = ownProps + const {index, profile, sandwichViewState} = activeProfileState + const flattenRecursion = useAppSelector(useCallback(state => state.flattenRecursion, [])) + const glCanvas = useAppSelector(useCallback(state => state.glCanvas, [])) + + if (!profile) throw new Error('profile missing') + if (!glCanvas) throw new Error('glCanvas missing') + const {callerCallee} = sandwichViewState + if (!callerCallee) throw new Error('callerCallee missing') + const {selectedFrame} = callerCallee + + const frameToColorBucket = getFrameToColorBucket(profile) + const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) + const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) + const canvasContext = getCanvasContext(glCanvas) + + const flamechart = getCalleeFlamegraph({ + calleeProfile: getCalleeProfile({profile, frame: selectedFrame, flattenRecursion}), + getColorBucketForFrame, + }) + const flamechartRenderer = getCalleeFlamegraphRenderer({canvasContext, flamechart}) + + return ( + + ) +}) diff --git a/src/views/color-chit.tsx b/src/views/color-chit.tsx index 0caae07d7..6f1ebae4b 100644 --- a/src/views/color-chit.tsx +++ b/src/views/color-chit.tsx @@ -1,4 +1,4 @@ -import {h, Component} from 'preact' +import {h} from 'preact' import {StyleSheet, css} from 'aphrodite' import {Colors, FontSize} from './style' @@ -6,10 +6,8 @@ interface ColorChitProps { color: string } -export class ColorChit extends Component { - render() { - return - } +export function ColorChit(props: ColorChitProps) { + return } const style = StyleSheet.create({ diff --git a/src/views/flamechart-detail-view.tsx b/src/views/flamechart-detail-view.tsx index 980031626..d222e701d 100644 --- a/src/views/flamechart-detail-view.tsx +++ b/src/views/flamechart-detail-view.tsx @@ -1,5 +1,5 @@ import {StyleDeclarationValue, css} from 'aphrodite' -import {h, Component} from 'preact' +import {h, Component, JSX} from 'preact' import {style} from './flamechart-style' import {formatPercent} from '../lib/utils' import {Frame, CallTreeNode} from '../lib/profile' diff --git a/src/views/flamechart-minimap-view.tsx b/src/views/flamechart-minimap-view.tsx index acaeb039f..73e83d4dd 100644 --- a/src/views/flamechart-minimap-view.tsx +++ b/src/views/flamechart-minimap-view.tsx @@ -26,7 +26,7 @@ enum DraggingMode { export class FlamechartMinimapView extends Component { container: Element | null = null - containerRef = (element?: Element) => { + containerRef = (element: Element | null) => { this.container = element || null } @@ -394,7 +394,7 @@ export class FlamechartMinimapView extends Component { + private overlayCanvasRef = (element: Element | null) => { if (element) { this.overlayCanvas = element as HTMLCanvasElement this.overlayCtx = this.overlayCanvas.getContext('2d') diff --git a/src/views/flamechart-pan-zoom-view.tsx b/src/views/flamechart-pan-zoom-view.tsx index bfa2e29cc..15c0936d8 100644 --- a/src/views/flamechart-pan-zoom-view.tsx +++ b/src/views/flamechart-pan-zoom-view.tsx @@ -52,7 +52,7 @@ export interface FlamechartPanZoomViewProps { export class FlamechartPanZoomView extends Component { private container: Element | null = null - private containerRef = (element?: Element) => { + private containerRef = (element: Element | null) => { this.container = element || null } @@ -65,7 +65,7 @@ export class FlamechartPanZoomView extends Component { + private overlayCanvasRef = (element: Element | null) => { if (element) { this.overlayCanvas = element as HTMLCanvasElement this.overlayCtx = this.overlayCanvas.getContext('2d') diff --git a/src/views/flamechart-view-container.tsx b/src/views/flamechart-view-container.tsx index 899a1aa1a..f9d620c65 100644 --- a/src/views/flamechart-view-container.tsx +++ b/src/views/flamechart-view-container.tsx @@ -1,11 +1,12 @@ +import {h} from 'preact' import {FlamechartID, FlamechartViewState} from '../store/flamechart-view-state' import {CanvasContext} from '../gl/canvas-context' import {Flamechart} from '../lib/flamechart' import {FlamechartRenderer, FlamechartRendererOptions} from '../gl/flamechart-renderer' -import {Dispatch, createContainer, ActionCreator} from '../lib/typed-redux' +import {ActionCreator} from '../lib/typed-redux' +import {useActionCreator} from '../lib/preact-redux' import {Frame, Profile, CallTreeNode} from '../lib/profile' import {memoizeByShallowEquality} from '../lib/utils' -import {ApplicationState} from '../store' import {FlamechartView} from './flamechart-view' import { getRowAtlas, @@ -17,6 +18,8 @@ import { import {ActiveProfileState} from './application' import {Vec2, Rect} from '../lib/math' import {actions} from '../store/actions' +import {memo} from 'preact/compat' +import {useCallback} from 'preact/hooks' interface FlamechartSetters { setLogicalSpaceViewportSize: (logicalSpaceViewportSize: Vec2) => void @@ -32,19 +35,19 @@ interface WithFlamechartContext { } & T } -export function createFlamechartSetters( - dispatch: Dispatch, - id: FlamechartID, - profileIndex: number, -): FlamechartSetters { - function wrapActionCreator( +export function useFlamechartSetters(id: FlamechartID, profileIndex: number): FlamechartSetters { + function useActionCreatorWithIndex( actionCreator: ActionCreator>, map: (t: T) => U, ): (t: T) => void { - return (t: T) => { - const args = Object.assign({}, map(t), {id}) - dispatch(actionCreator({profileIndex, args})) - } + const callback = useCallback( + (t: T) => { + const args = Object.assign({}, map(t), {id}) + return actionCreator({profileIndex, args}) + }, + [actionCreator, map], + ) + return useActionCreator(callback) } const { @@ -55,16 +58,22 @@ export function createFlamechartSetters( } = actions.flamechart return { - setNodeHover: wrapActionCreator(setHoveredNode, hover => ({hover})), - setLogicalSpaceViewportSize: wrapActionCreator( + setNodeHover: useActionCreatorWithIndex( + setHoveredNode, + useCallback(hover => ({hover}), []), + ), + setLogicalSpaceViewportSize: useActionCreatorWithIndex( setLogicalSpaceViewportSize, - logicalSpaceViewportSize => ({logicalSpaceViewportSize}), + useCallback(logicalSpaceViewportSize => ({logicalSpaceViewportSize}), []), ), - setConfigSpaceViewportRect: wrapActionCreator( + setConfigSpaceViewportRect: useActionCreatorWithIndex( setConfigSpaceViewportRect, - configSpaceViewportRect => ({configSpaceViewportRect}), + useCallback(configSpaceViewportRect => ({configSpaceViewportRect}), []), + ), + setSelectedNode: useActionCreatorWithIndex( + setSelectedNode, + useCallback(selectedNode => ({selectedNode}), []), ), - setSelectedNode: wrapActionCreator(setSelectedNode, selectedNode => ({selectedNode})), } } @@ -121,34 +130,33 @@ export interface FlamechartViewContainerProps { glCanvas: HTMLCanvasElement } -export const ChronoFlamechartView = createContainer( - FlamechartView, - (state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => { - const {activeProfileState, glCanvas} = ownProps - const {index, profile, chronoViewState} = activeProfileState - - const canvasContext = getCanvasContext(glCanvas) - const frameToColorBucket = getFrameToColorBucket(profile) - const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) - const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) - - const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame}) - const flamechartRenderer = getChronoViewFlamechartRenderer({ - canvasContext, - flamechart, - }) - - return { - renderInverted: false, - flamechart, - flamechartRenderer, - canvasContext, - getCSSColorForFrame, - ...createFlamechartSetters(dispatch, FlamechartID.CHRONO, index), - ...chronoViewState, - } - }, -) +export const ChronoFlamechartView = memo((props: FlamechartViewContainerProps) => { + const {activeProfileState, glCanvas} = props + const {index, profile, chronoViewState} = activeProfileState + + const canvasContext = getCanvasContext(glCanvas) + const frameToColorBucket = getFrameToColorBucket(profile) + const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) + const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) + + const flamechart = getChronoViewFlamechart({profile, getColorBucketForFrame}) + const flamechartRenderer = getChronoViewFlamechartRenderer({ + canvasContext, + flamechart, + }) + + return ( + + ) +}) export const getLeftHeavyFlamechart = memoizeByShallowEquality( ({ @@ -169,35 +177,34 @@ export const getLeftHeavyFlamechart = memoizeByShallowEquality( const getLeftHeavyFlamechartRenderer = createMemoizedFlamechartRenderer() -export const LeftHeavyFlamechartView = createContainer( - FlamechartView, - (state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => { - const {activeProfileState, glCanvas} = ownProps +export const LeftHeavyFlamechartView = memo((ownProps: FlamechartViewContainerProps) => { + const {activeProfileState, glCanvas} = ownProps - const {index, profile, leftHeavyViewState} = activeProfileState + const {index, profile, leftHeavyViewState} = activeProfileState - const canvasContext = getCanvasContext(glCanvas) - const frameToColorBucket = getFrameToColorBucket(profile) - const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) - const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) + const canvasContext = getCanvasContext(glCanvas) + const frameToColorBucket = getFrameToColorBucket(profile) + const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) + const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) - const flamechart = getLeftHeavyFlamechart({ - profile, - getColorBucketForFrame, - }) - const flamechartRenderer = getLeftHeavyFlamechartRenderer({ - canvasContext, - flamechart, - }) - - return { - renderInverted: false, - flamechart, - flamechartRenderer, - canvasContext, - getCSSColorForFrame, - ...createFlamechartSetters(dispatch, FlamechartID.LEFT_HEAVY, index), - ...leftHeavyViewState, - } - }, -) + const flamechart = getLeftHeavyFlamechart({ + profile, + getColorBucketForFrame, + }) + const flamechartRenderer = getLeftHeavyFlamechartRenderer({ + canvasContext, + flamechart, + }) + + return ( + + ) +}) diff --git a/src/views/flamechart-view.tsx b/src/views/flamechart-view.tsx index d47b9ceca..fa5178534 100644 --- a/src/views/flamechart-view.tsx +++ b/src/views/flamechart-view.tsx @@ -86,7 +86,7 @@ export class FlamechartView extends StatelessComponent { } container: HTMLDivElement | null = null - containerRef = (container?: Element) => { + containerRef = (container: Element | null) => { this.container = (container as HTMLDivElement) || null } diff --git a/src/views/flamechart-wrapper.tsx b/src/views/flamechart-wrapper.tsx index 62a6642dd..7fde61152 100644 --- a/src/views/flamechart-wrapper.tsx +++ b/src/views/flamechart-wrapper.tsx @@ -54,7 +54,7 @@ export class FlamechartWrapper extends StatelessComponent { ) } container: HTMLDivElement | null = null - containerRef = (container?: Element) => { + containerRef = (container: Element | null) => { this.container = (container as HTMLDivElement) || null } private setNodeHover = ( diff --git a/src/views/hovertip.tsx b/src/views/hovertip.tsx index 290e30535..58f4437c4 100644 --- a/src/views/hovertip.tsx +++ b/src/views/hovertip.tsx @@ -14,12 +14,7 @@ export class Hovertip extends Component { const width = containerSize.x const height = containerSize.y - const positionStyle: { - left?: number - right?: number - top?: number - bottom?: number - } = {} + const positionStyle: {[key: string]: number} = {} const OFFSET_FROM_MOUSE = 7 if (offset.x + OFFSET_FROM_MOUSE + Sizes.TOOLTIP_WIDTH_MAX < width) { diff --git a/src/views/inverted-caller-flamegraph-view.ts b/src/views/inverted-caller-flamegraph-view.ts deleted file mode 100644 index 7947b8132..000000000 --- a/src/views/inverted-caller-flamegraph-view.ts +++ /dev/null @@ -1,96 +0,0 @@ -import {memoizeByShallowEquality} from '../lib/utils' -import {Profile, Frame} from '../lib/profile' -import {Flamechart} from '../lib/flamechart' -import { - createMemoizedFlamechartRenderer, - FlamechartViewContainerProps, - createFlamechartSetters, -} from './flamechart-view-container' -import {createContainer, Dispatch} from '../lib/typed-redux' -import {ApplicationState} from '../store' -import { - getCanvasContext, - createGetColorBucketForFrame, - createGetCSSColorForFrame, - getProfileWithRecursionFlattened, - getFrameToColorBucket, -} from '../store/getters' -import {FlamechartID} from '../store/flamechart-view-state' -import {FlamechartWrapper} from './flamechart-wrapper' - -const getInvertedCallerProfile = memoizeByShallowEquality( - ({ - profile, - frame, - flattenRecursion, - }: { - profile: Profile - frame: Frame - flattenRecursion: boolean - }): Profile => { - let p = profile.getInvertedProfileForCallersOf(frame) - return flattenRecursion ? p.getProfileWithRecursionFlattened() : p - }, -) - -const getInvertedCallerFlamegraph = memoizeByShallowEquality( - ({ - invertedCallerProfile, - getColorBucketForFrame, - }: { - invertedCallerProfile: Profile - getColorBucketForFrame: (frame: Frame) => number - }): Flamechart => { - return new Flamechart({ - getTotalWeight: invertedCallerProfile.getTotalNonIdleWeight.bind(invertedCallerProfile), - forEachCall: invertedCallerProfile.forEachCallGrouped.bind(invertedCallerProfile), - formatValue: invertedCallerProfile.formatValue.bind(invertedCallerProfile), - getColorBucketForFrame, - }) - }, -) - -const getInvertedCallerFlamegraphRenderer = createMemoizedFlamechartRenderer({inverted: true}) - -export const InvertedCallerFlamegraphView = createContainer( - FlamechartWrapper, - (state: ApplicationState, dispatch: Dispatch, ownProps: FlamechartViewContainerProps) => { - const {activeProfileState} = ownProps - let {profile, sandwichViewState, index} = activeProfileState - let {flattenRecursion, glCanvas} = state - if (!profile) throw new Error('profile missing') - if (!glCanvas) throw new Error('glCanvas missing') - const {callerCallee} = sandwichViewState - if (!callerCallee) throw new Error('callerCallee missing') - const {selectedFrame} = callerCallee - - profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile - - const frameToColorBucket = getFrameToColorBucket(profile) - const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) - const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) - const canvasContext = getCanvasContext(glCanvas) - - const flamechart = getInvertedCallerFlamegraph({ - invertedCallerProfile: getInvertedCallerProfile({ - profile, - frame: selectedFrame, - flattenRecursion, - }), - getColorBucketForFrame, - }) - const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart}) - - return { - renderInverted: true, - flamechart, - flamechartRenderer, - canvasContext, - getCSSColorForFrame, - ...createFlamechartSetters(dispatch, FlamechartID.SANDWICH_INVERTED_CALLERS, index), - // This overrides the setSelectedNode specified in createFlamechartSettesr - setSelectedNode: () => {}, - ...callerCallee.invertedCallerFlamegraph, - } - }, -) diff --git a/src/views/inverted-caller-flamegraph-view.tsx b/src/views/inverted-caller-flamegraph-view.tsx new file mode 100644 index 000000000..a415bc371 --- /dev/null +++ b/src/views/inverted-caller-flamegraph-view.tsx @@ -0,0 +1,99 @@ +import {memoizeByShallowEquality, noop} from '../lib/utils' +import {Profile, Frame} from '../lib/profile' +import {Flamechart} from '../lib/flamechart' +import { + createMemoizedFlamechartRenderer, + FlamechartViewContainerProps, + useFlamechartSetters, +} from './flamechart-view-container' +import { + getCanvasContext, + createGetColorBucketForFrame, + createGetCSSColorForFrame, + getProfileWithRecursionFlattened, + getFrameToColorBucket, +} from '../store/getters' +import {FlamechartID} from '../store/flamechart-view-state' +import {useAppSelector} from '../store' +import {FlamechartWrapper} from './flamechart-wrapper' +import {h} from 'preact' +import {memo} from 'preact/compat' +import {useCallback} from 'preact/hooks' + +const getInvertedCallerProfile = memoizeByShallowEquality( + ({ + profile, + frame, + flattenRecursion, + }: { + profile: Profile + frame: Frame + flattenRecursion: boolean + }): Profile => { + let p = profile.getInvertedProfileForCallersOf(frame) + return flattenRecursion ? p.getProfileWithRecursionFlattened() : p + }, +) + +const getInvertedCallerFlamegraph = memoizeByShallowEquality( + ({ + invertedCallerProfile, + getColorBucketForFrame, + }: { + invertedCallerProfile: Profile + getColorBucketForFrame: (frame: Frame) => number + }): Flamechart => { + return new Flamechart({ + getTotalWeight: invertedCallerProfile.getTotalNonIdleWeight.bind(invertedCallerProfile), + forEachCall: invertedCallerProfile.forEachCallGrouped.bind(invertedCallerProfile), + formatValue: invertedCallerProfile.formatValue.bind(invertedCallerProfile), + getColorBucketForFrame, + }) + }, +) + +const getInvertedCallerFlamegraphRenderer = createMemoizedFlamechartRenderer({inverted: true}) + +export const InvertedCallerFlamegraphView = memo((ownProps: FlamechartViewContainerProps) => { + const {activeProfileState} = ownProps + let {profile, sandwichViewState, index} = activeProfileState + const flattenRecursion = useAppSelector(useCallback(state => state.flattenRecursion, [])) + const glCanvas = useAppSelector(useCallback(state => state.glCanvas, [])) + + if (!profile) throw new Error('profile missing') + if (!glCanvas) throw new Error('glCanvas missing') + const {callerCallee} = sandwichViewState + if (!callerCallee) throw new Error('callerCallee missing') + const {selectedFrame} = callerCallee + + profile = flattenRecursion ? getProfileWithRecursionFlattened(profile) : profile + + const frameToColorBucket = getFrameToColorBucket(profile) + const getColorBucketForFrame = createGetColorBucketForFrame(frameToColorBucket) + const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) + const canvasContext = getCanvasContext(glCanvas) + + const flamechart = getInvertedCallerFlamegraph({ + invertedCallerProfile: getInvertedCallerProfile({ + profile, + frame: selectedFrame, + flattenRecursion, + }), + getColorBucketForFrame, + }) + const flamechartRenderer = getInvertedCallerFlamegraphRenderer({canvasContext, flamechart}) + + return ( + + ) +}) diff --git a/src/views/profile-table-view.tsx b/src/views/profile-table-view.tsx index 1a7af8c40..95b3a86c4 100644 --- a/src/views/profile-table-view.tsx +++ b/src/views/profile-table-view.tsx @@ -1,4 +1,4 @@ -import {h, Component} from 'preact' +import {h, Component, JSX} from 'preact' import {StyleSheet, css} from 'aphrodite' import {Profile, Frame} from '../lib/profile' import {sortBy, formatPercent} from '../lib/utils' @@ -6,10 +6,12 @@ import {FontSize, Colors, Sizes, commonStyle} from './style' import {ColorChit} from './color-chit' import {ScrollableListView, ListItem} from './scrollable-list-view' import {actions} from '../store/actions' -import {Dispatch, createContainer} from '../lib/typed-redux' -import {ApplicationState} from '../store' import {createGetCSSColorForFrame, getFrameToColorBucket} from '../store/getters' import {ActiveProfileState} from './application' +import {useActionCreator} from '../lib/preact-redux' +import {useAppSelector} from '../store' +import {memo} from 'preact/compat' +import {useCallback, useMemo, useRef} from 'preact/hooks' export enum SortField { SYMBOL_NAME, @@ -31,14 +33,12 @@ interface HBarProps { perc: number } -class HBarDisplay extends Component { - render() { - return ( -
-
-
- ) - } +function HBarDisplay(props: HBarProps) { + return ( +
+
+
+ ) } interface SortIconProps { @@ -67,9 +67,54 @@ class SortIcon extends Component { } } +interface ProfileTableRowViewProps { + frame: Frame + index: number + profile: Profile + selectedFrame: Frame | null + setSelectedFrame: (f: Frame) => void + getCSSColorForFrame: (frame: Frame) => string +} + +const ProfileTableRowView = (props: ProfileTableRowViewProps) => { + const {frame, profile, index, selectedFrame, setSelectedFrame, getCSSColorForFrame} = props + const totalWeight = frame.getTotalWeight() + const selfWeight = frame.getSelfWeight() + const totalPerc = (100.0 * totalWeight) / profile.getTotalNonIdleWeight() + const selfPerc = (100.0 * selfWeight) / profile.getTotalNonIdleWeight() + + const selected = frame === selectedFrame + + // We intentionally use index rather than frame.key here as the tr key + // in order to re-use rows when sorting rather than creating all new elements. + return ( + + + {profile.formatValue(totalWeight)} ({formatPercent(totalPerc)}) + + + + {profile.formatValue(selfWeight)} ({formatPercent(selfPerc)}) + + + + + {frame.name} + + + ) +} + interface ProfileTableViewProps { profile: Profile - profileIndex: number selectedFrame: Frame | null getCSSColorForFrame: (frame: Frame) => string sortMethod: SortMethod @@ -77,81 +122,51 @@ interface ProfileTableViewProps { setSortMethod: (sortMethod: SortMethod) => void } -export class ProfileTableView extends Component { - renderRow(frame: Frame, index: number) { - const {profile, selectedFrame} = this.props - - const totalWeight = frame.getTotalWeight() - const selfWeight = frame.getSelfWeight() - const totalPerc = (100.0 * totalWeight) / profile.getTotalNonIdleWeight() - const selfPerc = (100.0 * selfWeight) / profile.getTotalNonIdleWeight() - - const selected = frame === selectedFrame - - // We intentionally use index rather than frame.key here as the tr key - // in order to re-use rows when sorting rather than creating all new elements. - return ( - - - {profile.formatValue(totalWeight)} ({formatPercent(totalPerc)}) - - - - {profile.formatValue(selfWeight)} ({formatPercent(selfPerc)}) - - - - - {frame.name} - - - ) - } - - onSortClick = (field: SortField, ev: MouseEvent) => { - ev.preventDefault() - - const {sortMethod} = this.props - - if (sortMethod.field == field) { - // Toggle - this.props.setSortMethod({ - field, - direction: - sortMethod.direction === SortDirection.ASCENDING - ? SortDirection.DESCENDING - : SortDirection.ASCENDING, - }) - } else { - // Set a sane default - switch (field) { - case SortField.SYMBOL_NAME: { - this.props.setSortMethod({field, direction: SortDirection.ASCENDING}) - break - } - case SortField.SELF: { - this.props.setSortMethod({field, direction: SortDirection.DESCENDING}) - break - } - case SortField.TOTAL: { - this.props.setSortMethod({field, direction: SortDirection.DESCENDING}) - break +export const ProfileTableView = memo((props: ProfileTableViewProps) => { + const { + profile, + sortMethod, + setSortMethod, + selectedFrame, + setSelectedFrame, + getCSSColorForFrame, + } = props + + const onSortClick = useCallback( + (field: SortField, ev: MouseEvent) => { + ev.preventDefault() + + if (sortMethod.field == field) { + // Toggle + setSortMethod({ + field, + direction: + sortMethod.direction === SortDirection.ASCENDING + ? SortDirection.DESCENDING + : SortDirection.ASCENDING, + }) + } else { + // Set a sane default + switch (field) { + case SortField.SYMBOL_NAME: { + setSortMethod({field, direction: SortDirection.ASCENDING}) + break + } + case SortField.SELF: { + setSortMethod({field, direction: SortDirection.DESCENDING}) + break + } + case SortField.TOTAL: { + setSortMethod({field, direction: SortDirection.DESCENDING}) + break + } } } - } - } - - private getFrameList = (): Frame[] => { - const {profile, sortMethod} = this.props + }, + [sortMethod, setSortMethod], + ) + const frameList = useMemo((): Frame[] => { const frameList: Frame[] = [] profile.forEachFrame(f => frameList.push(f)) @@ -177,89 +192,94 @@ export class ProfileTableView extends Component { } return frameList - } - - private listView: ScrollableListView | null = null - private listViewRef = (listView: ScrollableListView | null) => { - if (listView === this.listView) return - this.listView = listView - - const {selectedFrame} = this.props - if (!selectedFrame || !listView) return - const index = this.getFrameList().indexOf(selectedFrame) - if (index === -1) return - listView.scrollIndexIntoView(index) - } - - render() { - const {sortMethod} = this.props - - const frameList = this.getFrameList() - - const renderItems = (firstIndex: number, lastIndex: number) => { + }, [profile, sortMethod]) + + const listViewRef = useRef(null) + const listViewCallback = useCallback( + (listView: ScrollableListView | null) => { + if (listView === listViewRef.current) return + listViewRef.current = listView + if (!selectedFrame || !listView) return + const index = frameList.indexOf(selectedFrame) + if (index === -1) return + listView.scrollIndexIntoView(index) + }, + [listViewRef, selectedFrame, frameList], + ) + + const renderItems = useCallback( + (firstIndex: number, lastIndex: number) => { const rows: JSX.Element[] = [] for (let i = firstIndex; i <= lastIndex; i++) { - rows.push(this.renderRow(frameList[i], i)) + rows.push( + ProfileTableRowView({ + frame: frameList[i], + index: i, + profile: profile, + selectedFrame: selectedFrame, + setSelectedFrame: setSelectedFrame, + getCSSColorForFrame: getCSSColorForFrame, + }), + ) } return {rows}
- } - - const listItems: ListItem[] = frameList.map(f => ({size: Sizes.FRAME_HEIGHT})) - - return ( -
- - - - - - - - -
this.onSortClick(SortField.TOTAL, ev)} - > - - Total - this.onSortClick(SortField.SELF, ev)} - > - - Self - this.onSortClick(SortField.SYMBOL_NAME, ev)} - > - - Symbol Name -
- -
- ) - } -} + }, + [frameList, profile, selectedFrame, setSelectedFrame, getCSSColorForFrame], + ) + + const listItems: ListItem[] = frameList.map(f => ({size: Sizes.FRAME_HEIGHT})) + + const onTotalClick = useCallback((ev: MouseEvent) => onSortClick(SortField.TOTAL, ev), [ + onSortClick, + ]) + const onSelfClick = useCallback((ev: MouseEvent) => onSortClick(SortField.SELF, ev), [ + onSortClick, + ]) + const onSymbolNameClick = useCallback( + (ev: MouseEvent) => onSortClick(SortField.SYMBOL_NAME, ev), + [onSortClick], + ) + + return ( +
+ + + + + + + + +
+ + Total + + + Self + + + Symbol Name +
+ +
+ ) +}) const style = StyleSheet.create({ profileTableView: { @@ -333,34 +353,34 @@ interface ProfileTableViewContainerProps { activeProfileState: ActiveProfileState } -export const ProfileTableViewContainer = createContainer( - ProfileTableView, - (state: ApplicationState, dispatch: Dispatch, ownProps: ProfileTableViewContainerProps) => { - const {activeProfileState} = ownProps - const {profile, sandwichViewState, index} = activeProfileState - if (!profile) throw new Error('profile missing') - const {tableSortMethod} = state - const {callerCallee} = sandwichViewState - const selectedFrame = callerCallee ? callerCallee.selectedFrame : null - const frameToColorBucket = getFrameToColorBucket(profile) - const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) - - const setSelectedFrame = (selectedFrame: Frame | null) => { - dispatch(actions.sandwichView.setSelectedFrame({profileIndex: index, args: selectedFrame})) - } - - const setSortMethod = (sortMethod: SortMethod) => { - dispatch(actions.sandwichView.setTableSortMethod(sortMethod)) - } - - return { - profile, - profileIndex: activeProfileState.index, - selectedFrame, - getCSSColorForFrame, - sortMethod: tableSortMethod, - setSelectedFrame, - setSortMethod, - } - }, -) +export const ProfileTableViewContainer = memo((ownProps: ProfileTableViewContainerProps) => { + const {activeProfileState} = ownProps + const {profile, sandwichViewState, index} = activeProfileState + if (!profile) throw new Error('profile missing') + const tableSortMethod = useAppSelector(useCallback(state => state.tableSortMethod, [])) + const {callerCallee} = sandwichViewState + const selectedFrame = callerCallee ? callerCallee.selectedFrame : null + const frameToColorBucket = getFrameToColorBucket(profile) + const getCSSColorForFrame = createGetCSSColorForFrame(frameToColorBucket) + + const setSelectedFrameWithIndex = useCallback( + (selectedFrame: Frame | null) => { + return actions.sandwichView.setSelectedFrame({profileIndex: index, args: selectedFrame}) + }, + [index], + ) + + const setSelectedFrame = useActionCreator(setSelectedFrameWithIndex) + const setSortMethod = useActionCreator(actions.sandwichView.setTableSortMethod) + + return ( + + ) +}) diff --git a/src/views/sandwich-view.tsx b/src/views/sandwich-view.tsx index 06cbd637d..7eb54d749 100644 --- a/src/views/sandwich-view.tsx +++ b/src/views/sandwich-view.tsx @@ -1,14 +1,16 @@ import {Frame} from '../lib/profile' import {StyleSheet, css} from 'aphrodite' import {ProfileTableViewContainer} from './profile-table-view' -import {h} from 'preact' +import {h, JSX} from 'preact' +import {memo} from 'preact/compat' +import {useCallback} from 'preact/hooks' import {commonStyle, Sizes, Colors, FontSize} from './style' import {actions} from '../store/actions' -import {createContainer, Dispatch, StatelessComponent} from '../lib/typed-redux' -import {ApplicationState} from '../store' +import {StatelessComponent} from '../lib/typed-redux' import {InvertedCallerFlamegraphView} from './inverted-caller-flamegraph-view' import {CalleeFlamegraphView} from './callee-flamegraph-view' import {ActiveProfileState} from './application' +import {useDispatch} from '../lib/preact-redux' interface SandwichViewProps { selectedFrame: Frame | null @@ -122,28 +124,30 @@ interface SandwichViewContainerProps { glCanvas: HTMLCanvasElement } -export const SandwichViewContainer = createContainer( - SandwichView, - (state: ApplicationState, dispatch: Dispatch, ownProps: SandwichViewContainerProps) => { - const {activeProfileState, glCanvas} = ownProps - const {sandwichViewState, index} = activeProfileState - const {callerCallee} = sandwichViewState +export const SandwichViewContainer = memo((ownProps: SandwichViewContainerProps) => { + const {activeProfileState, glCanvas} = ownProps + const {sandwichViewState, index} = activeProfileState + const {callerCallee} = sandwichViewState - const setSelectedFrame = (selectedFrame: Frame | null) => { + const dispatch = useDispatch() + const setSelectedFrame = useCallback( + (selectedFrame: Frame | null) => { dispatch( actions.sandwichView.setSelectedFrame({ profileIndex: index, args: selectedFrame, }), ) - } - - return { - activeProfileState: activeProfileState, - glCanvas, - setSelectedFrame, - selectedFrame: callerCallee ? callerCallee.selectedFrame : null, - profileIndex: index, - } - }, -) + }, + [dispatch, index], + ) + return ( + + ) +}) diff --git a/src/views/scrollable-list-view.tsx b/src/views/scrollable-list-view.tsx index c608dd8e2..8636d0ac8 100644 --- a/src/views/scrollable-list-view.tsx +++ b/src/views/scrollable-list-view.tsx @@ -1,7 +1,7 @@ // A simple implementation of an efficient scrolling list view which // renders only items within the viewport + a couple extra items. -import {h, Component} from 'preact' +import {h, Component, JSX} from 'preact' export interface ListItem { size: number @@ -38,7 +38,7 @@ export class ScrollableListView extends Component< } private viewport: HTMLDivElement | null = null - private viewportRef = (viewport?: Element) => { + private viewportRef = (viewport: Element | null) => { this.viewport = (viewport as HTMLDivElement) || null } diff --git a/src/views/toolbar.tsx b/src/views/toolbar.tsx new file mode 100644 index 000000000..e60e73fe0 --- /dev/null +++ b/src/views/toolbar.tsx @@ -0,0 +1,230 @@ +import {ApplicationProps} from './application' +import {ViewMode} from '../store' +import {h, JSX, Fragment} from 'preact' +import {useCallback} from 'preact/hooks' +import {StyleSheet, css} from 'aphrodite' +import {Sizes, Colors, FontFamily, FontSize, Duration} from './style' + +interface ToolbarProps extends ApplicationProps { + browseForFile(): void + saveFile(): void +} + +function useSetViewMode(setViewMode: (viewMode: ViewMode) => void, viewMode: ViewMode) { + return useCallback(() => setViewMode(viewMode), [setViewMode, viewMode]) +} + +function ToolbarLeftContent(props: ToolbarProps) { + const setChronoFlameChart = useSetViewMode(props.setViewMode, ViewMode.CHRONO_FLAME_CHART) + const setLeftHeavyFlameGraph = useSetViewMode(props.setViewMode, ViewMode.LEFT_HEAVY_FLAME_GRAPH) + const setSandwichView = useSetViewMode(props.setViewMode, ViewMode.SANDWICH_VIEW) + + if (!props.activeProfileState) return null + + return ( +
+
+ 🕰Time Order +
+
+ ⬅️Left Heavy +
+
+ 🥪Sandwich +
+
+ ) +} + +function ToolbarCenterContent(props: ToolbarProps): JSX.Element { + const {activeProfileState, profileGroup} = props + if (activeProfileState && profileGroup) { + const {index} = activeProfileState + if (profileGroup.profiles.length === 1) { + return {activeProfileState.profile.getName()} + } else { + function makeNavButton(content: string, disabled: boolean, onClick: () => void) { + return ( + + ) + } + + const prevButton = makeNavButton('⬅️', index === 0, () => + props.setProfileIndexToView(index - 1), + ) + const nextButton = makeNavButton('➡️', index >= profileGroup.profiles.length - 1, () => + props.setProfileIndexToView(index + 1), + ) + + return ( +
+ {prevButton} + {activeProfileState.profile.getName()}{' '} + + ({activeProfileState.index + 1}/{profileGroup.profiles.length}) + + {nextButton} +
+ ) + } + } + return {'🔬speedscope'} +} + +function ToolbarRightContent(props: ToolbarProps) { + const importFile = ( +
+ ⤵️Import +
+ ) + const help = ( + + ) + + return ( +
+ {props.activeProfileState && ( +
+ ⤴️Export +
+ )} + {importFile} + {help} +
+ ) +} + +export function Toolbar(props: ToolbarProps) { + return ( +
+ + + +
+ ) +} + +const style = StyleSheet.create({ + toolbar: { + height: Sizes.TOOLBAR_HEIGHT, + flexShrink: 0, + background: Colors.BLACK, + color: Colors.WHITE, + textAlign: 'center', + fontFamily: FontFamily.MONOSPACE, + fontSize: FontSize.TITLE, + lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`, + userSelect: 'none', + }, + toolbarLeft: { + position: 'absolute', + height: Sizes.TOOLBAR_HEIGHT, + overflow: 'hidden', + top: 0, + left: 0, + marginRight: 2, + textAlign: 'left', + }, + toolbarCenter: { + paddingTop: 1, + height: Sizes.TOOLBAR_HEIGHT, + }, + toolbarRight: { + height: Sizes.TOOLBAR_HEIGHT, + overflow: 'hidden', + position: 'absolute', + top: 0, + right: 0, + marginRight: 2, + textAlign: 'right', + }, + toolbarProfileIndex: { + color: Colors.LIGHT_GRAY, + }, + toolbarProfileNavButton: { + opacity: 0.8, + fontSize: FontSize.TITLE, + lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`, + ':hover': { + opacity: 1.0, + }, + background: 'none', + border: 'none', + padding: 0, + marginLeft: '0.3em', + marginRight: '0.3em', + transition: `all ${Duration.HOVER_CHANGE} ease-in`, + }, + toolbarProfileNavButtonDisabled: { + opacity: 0.5, + ':hover': { + opacity: 0.5, + }, + }, + toolbarTab: { + background: Colors.DARK_GRAY, + marginTop: Sizes.SEPARATOR_HEIGHT, + height: Sizes.TOOLBAR_TAB_HEIGHT, + lineHeight: `${Sizes.TOOLBAR_TAB_HEIGHT}px`, + paddingLeft: 2, + paddingRight: 8, + display: 'inline-block', + marginLeft: 2, + transition: `all ${Duration.HOVER_CHANGE} ease-in`, + ':hover': { + background: Colors.GRAY, + }, + }, + toolbarTabActive: { + background: Colors.BRIGHT_BLUE, + ':hover': { + background: Colors.BRIGHT_BLUE, + }, + }, + emoji: { + display: 'inline-block', + verticalAlign: 'middle', + paddingTop: '0px', + marginRight: '0.3em', + }, + noLinkStyle: { + textDecoration: 'none', + color: 'inherit', + }, +})