From 9d4e184260338fcfa6f015fe2ce709b9b3bda2d2 Mon Sep 17 00:00:00 2001 From: "Yuan (Bob) Gong" Date: Sat, 12 Oct 2019 17:22:38 +0800 Subject: [PATCH] [Frontend] Fix cannot copy logs in LogViewer when scrolling (#2370) * Fix LogViewer logs cannot be selected when scrolling. * LogViewer only follows new log when user scrolled to bottom of logs * Fix compile issues * Improve performance when log size is huge * Fix snapshot tests: upgraded enzyme to 3.10.0 to work with React.memo; Changed snapshots to mount + getDOMNode to be invariant to internal refactoring * Fix type checking failure --- frontend/package-lock.json | 369 +++++++++++++++--- frontend/package.json | 8 +- frontend/src/components/LogViewer.test.tsx | 32 +- frontend/src/components/LogViewer.tsx | 158 +++++--- .../__snapshots__/LogViewer.test.tsx.snap | 306 +++++---------- frontend/tsconfig.json | 3 +- 6 files changed, 526 insertions(+), 350 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d939c24ca74..53a505b0677 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -155,10 +155,13 @@ } }, "@types/cheerio": { - "version": "0.22.10", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.10.tgz", - "integrity": "sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg==", - "dev": true + "version": "0.22.13", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.13.tgz", + "integrity": "sha512-OZd7dCUOUkiTorf97vJKwZnSja/DmHfuBAroe1kREZZTCf/tlFecwHhsOos3uVHxeKGZDwzolIrCUApClkdLuA==", + "dev": true, + "requires": { + "@types/node": "*" + } }, "@types/connect": { "version": "3.4.32", @@ -442,9 +445,9 @@ "dev": true }, "@types/enzyme": { - "version": "3.1.15", - "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.1.15.tgz", - "integrity": "sha512-6b4JWgV+FNec1c4+8HauGbXg5gRc1oQK93t2+4W+bHjG/PzO+iPvagY6d6bXAZ+t+ps51Zb2F9LQ4vl0S0Epog==", + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.3.tgz", + "integrity": "sha512-f/Kcb84sZOSZiBPCkr4He9/cpuSLcKRyQaEE20Q30Prx0Dn6wcyMAWI0yofL6yvd9Ht9G7EVkQeRqK0n5w8ILw==", "dev": true, "requires": { "@types/cheerio": "*", @@ -452,9 +455,9 @@ } }, "@types/enzyme-adapter-react-16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.3.tgz", - "integrity": "sha512-9eRLBsC/Djkys05BdTWgav8v6fSCjyzjNuLwG2sfa2b2g/VAN10luP0zB0VwtOWFQ0LGjIboJJvIsVdU5gqRmg==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz", + "integrity": "sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg==", "dev": true, "requires": { "@types/enzyme": "*" @@ -744,6 +747,43 @@ "es6-promisify": "^5.0.0" } }, + "airbnb-prop-types": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.15.0.tgz", + "integrity": "sha512-jUh2/hfKsRjNFC4XONQrxo/n/3GG4Tn6Hl0WlFQN5PY9OMC9loSCoAYKnZsWaP8wEfd5xcrPloK0Zg6iS1xwVA==", + "dev": true, + "requires": { + "array.prototype.find": "^2.1.0", + "function.prototype.name": "^1.1.1", + "has": "^1.0.3", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object.assign": "^4.1.0", + "object.entries": "^1.1.0", + "prop-types": "^15.7.2", + "prop-types-exact": "^1.2.0", + "react-is": "^16.9.0" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react-is": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", + "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==", + "dev": true + } + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -1006,15 +1046,51 @@ "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" }, + "array.prototype.find": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.0.tgz", + "integrity": "sha512-Wn41+K1yuO5p7wRZDl7890c3xvv5UBrfVXTVIe28rSQb6LS0fZMDrQB6PAcxQFRFy6vJTLDc3A2+3CjQdzVKRg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.13.0" + } + }, "array.prototype.flat": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz", - "integrity": "sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.2.tgz", + "integrity": "sha512-VXjh7lAL4KXKF2hY4FnEW9eRW6IhdvFW1sN/JwLbmECbCgACCnBHNyP3lFiYuttr0jxRN9Bsc5+G27dMseSWqQ==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.15.0", "function-bind": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", + "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.0", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + } } }, "arrify": { @@ -2701,19 +2777,29 @@ "dev": true }, "cheerio": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", - "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz", + "integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==", "dev": true, "requires": { "css-select": "~1.2.0", - "dom-serializer": "~0.1.0", + "dom-serializer": "~0.1.1", "entities": "~1.1.1", "htmlparser2": "^3.9.1", "lodash": "^4.15.0", "parse5": "^3.0.1" }, "dependencies": { + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "dev": true, + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, "domhandler": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", @@ -2724,17 +2810,17 @@ } }, "htmlparser2": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.0.tgz", - "integrity": "sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", "dev": true, "requires": { - "domelementtype": "^1.3.0", + "domelementtype": "^1.3.1", "domhandler": "^2.3.0", "domutils": "^1.5.1", "entities": "^1.1.1", "inherits": "^2.0.1", - "readable-stream": "^3.0.6" + "readable-stream": "^3.1.1" } }, "parse5": { @@ -2747,9 +2833,9 @@ } }, "readable-stream": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.1.1.tgz", - "integrity": "sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -4394,18 +4480,20 @@ "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" }, "enzyme": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.8.0.tgz", - "integrity": "sha512-bfsWo5nHyZm1O1vnIsbwdfhU989jk+squU9NKvB+Puwo5j6/Wg9pN5CO0YJelm98Dao3NPjkDZk+vvgwpMwYxw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.10.0.tgz", + "integrity": "sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg==", "dev": true, "requires": { "array.prototype.flat": "^1.2.1", "cheerio": "^1.0.0-rc.2", "function.prototype.name": "^1.1.0", "has": "^1.0.3", + "html-element-map": "^1.0.0", "is-boolean-object": "^1.0.0", "is-callable": "^1.1.4", "is-number-object": "^1.0.3", + "is-regex": "^1.0.4", "is-string": "^1.0.4", "is-subset": "^0.1.1", "lodash.escape": "^4.0.1", @@ -4421,30 +4509,94 @@ } }, "enzyme-adapter-react-16": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.7.1.tgz", - "integrity": "sha512-OQXKgfHWyHN3sFu2nKj3mhgRcqIPIJX6aOzq5AHVFES4R9Dw/vCBZFMPyaG81g2AZ5DogVh39P3MMNUbqNLTcw==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.1.tgz", + "integrity": "sha512-yMPxrP3vjJP+4wL/qqfkT6JAIctcwKF+zXO6utlGPgUJT2l4tzrdjMDWGd/Pp1BjHBcljhN24OzNEGRteibJhA==", "dev": true, "requires": { - "enzyme-adapter-utils": "^1.9.0", - "function.prototype.name": "^1.1.0", + "enzyme-adapter-utils": "^1.12.1", + "enzyme-shallow-equal": "^1.0.0", + "has": "^1.0.3", "object.assign": "^4.1.0", - "object.values": "^1.0.4", - "prop-types": "^15.6.2", - "react-is": "^16.6.1", - "react-test-renderer": "^16.0.0-0" + "object.values": "^1.1.0", + "prop-types": "^15.7.2", + "react-is": "^16.10.2", + "react-test-renderer": "^16.0.0-0", + "semver": "^5.7.0" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react-is": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", + "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "enzyme-adapter-utils": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.9.1.tgz", - "integrity": "sha512-LWc88BbKztLXlpRf5Ba/pSMJRaNezAwZBvis3N/IuB65ltZEh2E2obWU9B36pAbw7rORYeBUuqc79OL17ZzN1A==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.12.1.tgz", + "integrity": "sha512-KWiHzSjZaLEoDCOxY8Z1RAbUResbqKN5bZvenPbfKtWorJFVETUw754ebkuCQ3JKm0adx1kF8JaiR+PHPiP47g==", "dev": true, "requires": { - "function.prototype.name": "^1.1.0", + "airbnb-prop-types": "^2.15.0", + "function.prototype.name": "^1.1.1", "object.assign": "^4.1.0", - "prop-types": "^15.6.2", - "semver": "^5.6.0" + "object.fromentries": "^2.0.1", + "prop-types": "^15.7.2", + "semver": "^5.7.0" + }, + "dependencies": { + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "dev": true, + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react-is": { + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", + "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "enzyme-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.0.tgz", + "integrity": "sha512-VUf+q5o1EIv2ZaloNQQtWCJM9gpeux6vudGVH6vLmfPXFLRuxl5+Aq3U260wof9nn0b0i+P5OEUXm1vnxkRpXQ==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object-is": "^1.0.1" } }, "enzyme-to-json": { @@ -5790,16 +5942,23 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "function.prototype.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", - "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.1.tgz", + "integrity": "sha512-e1NzkiJuw6xqVH7YSdiW/qDHebcmMhPNe6w+4ZYYEg0VA+LaLzx37RimbPLuonHhYGFGPx1ME2nSi74JiaCr/Q==", "dev": true, "requires": { - "define-properties": "^1.1.2", + "define-properties": "^1.1.3", "function-bind": "^1.1.1", - "is-callable": "^1.1.3" + "functions-have-names": "^1.1.1", + "is-callable": "^1.1.4" } }, + "functions-have-names": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.1.1.tgz", + "integrity": "sha512-U0kNHUoxwPNPWOJaMG7Z00d4a/qZVrFtzWJRaK8V9goaVOCXBSQSJpt3MYGNtkScKEBKovxLjnNdC9MlXwo5Pw==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -6257,6 +6416,23 @@ "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==" }, + "html-element-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.1.0.tgz", + "integrity": "sha512-iqiG3dTZmy+uUaTmHarTL+3/A2VW9ox/9uasKEZC+R/wAtUrTcRlXPSaPqsnWPfIu8wqn09jQNwMRqzL54jSYA==", + "dev": true, + "requires": { + "array-filter": "^1.0.0" + }, + "dependencies": { + "array-filter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-1.0.0.tgz", + "integrity": "sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=", + "dev": true + } + } + }, "html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -8962,9 +9138,9 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" }, "nearley": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.16.0.tgz", - "integrity": "sha512-Tr9XD3Vt/EujXbZBv6UAHYoLUSMQAxSsTnm9K3koXzjzNWY195NqALeyrzLZBKzAkL3gl92BcSogqrHjD8QuUg==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.19.0.tgz", + "integrity": "sha512-2v52FTw7RPqieZr3Gth1luAXZR7Je6q3KaDHY5bjl/paDUdMu35fZ8ICNgiYJRr3tf3NMvIQQR1r27AvEr9CRA==", "dev": true, "requires": { "commander": "^2.19.0", @@ -9255,6 +9431,44 @@ "has": "^1.0.3" } }, + "object.fromentries": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.1.tgz", + "integrity": "sha512-PUQv8Hbg3j2QX0IQYv3iAGCbGcu4yY4KQ92/dhA4sFSixBmSmp13UpDLs6jGK8rBtbmhNNIK99LD2k293jpiGA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.15.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + }, + "dependencies": { + "es-abstract": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", + "integrity": "sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.0", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-inspect": "^1.6.0", + "object-keys": "^1.1.1", + "string.prototype.trimleft": "^2.1.0", + "string.prototype.trimright": "^2.1.0" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + } + } + }, "object.getownpropertydescriptors": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", @@ -10938,6 +11152,17 @@ "object-assign": "^4.1.1" } }, + "prop-types-exact": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", + "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object.assign": "^4.1.0", + "reflect.ownkeys": "^0.2.0" + } + }, "proxy-addr": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", @@ -11648,6 +11873,12 @@ "balanced-match": "^0.4.2" } }, + "reflect.ownkeys": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", + "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=", + "dev": true + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -12728,14 +12959,34 @@ } }, "string.prototype.trim": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", - "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.0.tgz", + "integrity": "sha512-9EIjYD/WdlvLpn987+ctkLf0FfvBefOCuiEr2henD8X+7jfwPnyvTdmW8OJhj5p+M0/96mBdynLWkxUr+rHlpg==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.0", - "function-bind": "^1.0.2" + "define-properties": "^1.1.3", + "es-abstract": "^1.13.0", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimleft": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz", + "integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz", + "integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" } }, "string_decoder": { diff --git a/frontend/package.json b/frontend/package.json index 1756ea907a1..dd755986335 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -60,8 +60,8 @@ "@types/d3": "^5.0.0", "@types/d3-dsv": "^1.0.33", "@types/dagre": "^0.7.40", - "@types/enzyme": "^3.1.15", - "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/enzyme": "^3.10.3", + "@types/enzyme-adapter-react-16": "^1.0.5", "@types/express": "^4.16.0", "@types/http-proxy-middleware": "^0.17.5", "@types/jest": "^23.3.2", @@ -75,8 +75,8 @@ "@types/react-virtualized": "^9.18.7", "backstopjs": "^3.5.16", "coveralls": "^3.0.2", - "enzyme": "^3.7.0", - "enzyme-adapter-react-16": "^1.5.0", + "enzyme": "^3.10.0", + "enzyme-adapter-react-16": "^1.15.1", "enzyme-to-json": "^3.3.4", "react-router-test-context": "^0.1.0", "react-test-renderer": "^16.5.2", diff --git a/frontend/src/components/LogViewer.test.tsx b/frontend/src/components/LogViewer.test.tsx index eaadb2317df..b84f999bfd9 100644 --- a/frontend/src/components/LogViewer.test.tsx +++ b/frontend/src/components/LogViewer.test.tsx @@ -26,14 +26,14 @@ describe('LogViewer', () => { it('renders one log line', () => { const logLines = ['first line']; const logViewer = new LogViewer({ logLines }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders two log lines', () => { const logLines = ['first line', 'second line']; const logViewer = new LogViewer({ logLines }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); @@ -48,7 +48,7 @@ describe('LogViewer', () => { `with desktop publishing software like Aldus PageMaker including versions` + `of Lorem Ipsum.`; const logViewer = new LogViewer({ logLines: [line] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); @@ -63,35 +63,35 @@ describe('LogViewer', () => { with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.`; const logViewer = new LogViewer({ logLines: line.split('\n') }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('linkifies standalone urls', () => { const logLines = ['this string: http://path.com is a url']; const logViewer = new LogViewer({ logLines }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('linkifies standalone https urls', () => { const logLines = ['this string: https://path.com is a url']; const logViewer = new LogViewer({ logLines }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('linkifies substring urls', () => { const logLines = ['this string:http://path.com is a url']; const logViewer = new LogViewer({ logLines }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('does not linkify non http/https urls', () => { const logLines = ['this string: gs://path is a GCS path']; const logViewer = new LogViewer({ logLines }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); @@ -105,49 +105,49 @@ describe('LogViewer', () => { it('renders a row with given index as line number', () => { const logViewer = new LogViewer({ logLines: ['line1', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders a row with error', () => { const logViewer = new LogViewer({ logLines: ['line1 with error', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders a row with upper case error', () => { const logViewer = new LogViewer({ logLines: ['line1 with ERROR', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders a row with error word as substring', () => { const logViewer = new LogViewer({ logLines: ['line1 with errorWord', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders a row with warning', () => { const logViewer = new LogViewer({ logLines: ['line1 with warning', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders a row with warn', () => { const logViewer = new LogViewer({ logLines: ['line1 with warn', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders a row with upper case warning', () => { const logViewer = new LogViewer({ logLines: ['line1 with WARNING', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); it('renders a row with warning word as substring', () => { const logViewer = new LogViewer({ logLines: ['line1 with warning:something', 'line2'] }); - const tree = shallow((logViewer as any)._rowRenderer({ index: 0 })); + const tree = mount((logViewer as any)._rowRenderer({ index: 0 })).getDOMNode(); expect(tree).toMatchSnapshot(); }); }); diff --git a/frontend/src/components/LogViewer.tsx b/frontend/src/components/LogViewer.tsx index 4030dc2bb15..33fb17ebaf3 100644 --- a/frontend/src/components/LogViewer.tsx +++ b/frontend/src/components/LogViewer.tsx @@ -18,6 +18,7 @@ import * as React from 'react'; import { List, AutoSizer, ListRowProps } from 'react-virtualized'; import { fontsize, fonts } from '../Css'; import { stylesheet } from 'typestyle'; +import { OverscanIndicesGetter } from 'react-virtualized/dist/es/Grid'; const css = stylesheet({ a: { @@ -47,11 +48,6 @@ const css = stylesheet({ userSelect: 'none', }, root: { - $nest: { - '& .ReactVirtualized__Grid__innerScrollContainer': { - overflow: 'auto !important', - }, - }, backgroundColor: '#222', color: '#fff', fontFamily: fonts.code, @@ -66,27 +62,83 @@ interface LogViewerProps { logLines: string[]; } -class LogViewer extends React.Component { +// Use the same amount of overscan above and below visible rows. +// +// Why: +// * Default behavior is that when we scroll to one direction, content off +// screen on the other direction is unmounted from browser immediately. This +// caused a bug when selecting lines + scrolling. +// * With new behavior implemented below: we are now overscanning on both +// directions disregard of which direction user is scrolling to, we can ensure +// lines not exceeding maximum overscanRowCount lines off screen are still +// selectable. +const overscanOnBothDirections: OverscanIndicesGetter = ({ + direction, // One of "horizontal" or "vertical" + cellCount, // Number of rows or columns in the current axis + scrollDirection, // 1 (forwards) or -1 (backwards) + overscanCellsCount, // Maximum number of cells to over-render in either direction + startIndex, // Begin of range of visible cells + stopIndex // End of range of visible cells +}) => { + return ({ + overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), + overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount) + }); +}; + +interface LogViewerState { + followNewLogs: boolean; +} + +class LogViewer extends React.Component { + public state = { + followNewLogs: true, + }; + private _rootRef = React.createRef(); public componentDidMount(): void { - this._scrollToEnd(); + // Wait until the next frame to scroll to bottom, because doms haven't been + // rendered when running this. + setTimeout(() => { + this._scrollToEnd(); + }); } public componentDidUpdate(): void { - this._scrollToEnd(); + if (this.state.followNewLogs) { + this._scrollToEnd(); + } } public render(): JSX.Element { return {({ height, width }) => ( - + overscanIndicesGetter={overscanOnBothDirections} + overscanRowCount={400 /* make this large, so selecting maximum 400 lines is supported */} + rowRenderer={this._rowRenderer.bind(this)} + onScroll={this.handleScroll} + /> )} ; } + private handleScroll = ( + info: { clientHeight: number; scrollHeight: number; scrollTop: number } + ) => { + const offsetTolerance = 20; // pixels + const isScrolledToBottom = info.scrollHeight - info.scrollTop - info.clientHeight <= offsetTolerance; + if (isScrolledToBottom !== this.state.followNewLogs) { + this.setState({ + followNewLogs: isScrolledToBottom, + }); + } + }; + private _scrollToEnd(): void { const root = this._rootRef.current; if (root) { @@ -96,56 +148,60 @@ class LogViewer extends React.Component { private _rowRenderer(props: ListRowProps): React.ReactNode { const { style, key, index } = props; + const line = this.props.logLines[index]; return (
- {index + 1} - - - {this._parseLine(this.props.logLines[index]).map((piece, p) => ( - {piece} - ))} - +
); } +} - private _getLineStyle(index: number): React.CSSProperties { - const line = this.props.logLines[index]; - const lineLowerCase = line.toLowerCase(); - if (lineLowerCase.indexOf('error') > -1 || lineLowerCase.indexOf('fail') > -1) { - return { - backgroundColor: '#700000', - color: 'white', - }; - } else if (lineLowerCase.indexOf('warn') > -1) { - return { - backgroundColor: '#545400', - color: 'white', - }; - } else { - return {}; - } +const LogLine: React.FC<{ index: number, line: string }> = ({ index, line }) => + <> + {index + 1} + + {parseLine(line).map((piece, p) => ({piece}))} + + ; +// improve performance when rerendering, because we render a lot of logs +const MemoedLogLine = React.memo(LogLine); + +function getLineStyle(line: string): React.CSSProperties { + const lineLowerCase = line.toLowerCase(); + if (lineLowerCase.indexOf('error') > -1 || lineLowerCase.indexOf('fail') > -1) { + return { + backgroundColor: '#700000', + color: 'white', + }; + } else if (lineLowerCase.indexOf('warn') > -1) { + return { + backgroundColor: '#545400', + color: 'white', + }; + } else { + return {}; } +} - private _parseLine(line: string): React.ReactNode[] { - // Linkify URLs starting with http:// or https:// - const urlPattern = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; - let lastMatch = 0; - let match = urlPattern.exec(line); - const nodes = []; - while (match) { - // Append all text before URL match - nodes.push({line.substr(lastMatch, match.index)}); - // Append URL via an anchor element - nodes.push({match[0]}); - - lastMatch = match.index + match[0].length; - match = urlPattern.exec(line); - } - // Append all text after final URL - nodes.push({line.substr(lastMatch)}); - return nodes; +function parseLine(line: string): React.ReactNode[] { + // Linkify URLs starting with http:// or https:// + const urlPattern = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; + let lastMatch = 0; + let match = urlPattern.exec(line); + const nodes = []; + while (match) { + // Append all text before URL match + nodes.push({line.substr(lastMatch, match.index)}); + // Append URL via an anchor element + nodes.push({match[0]}); + + lastMatch = match.index + match[0].length; + match = urlPattern.exec(line); } + // Append all text after final URL + nodes.push({line.substr(lastMatch)}); + return nodes; } export default LogViewer; diff --git a/frontend/src/components/__snapshots__/LogViewer.test.tsx.snap b/frontend/src/components/__snapshots__/LogViewer.test.tsx.snap index d3045f95a97..3bf9b280bce 100644 --- a/frontend/src/components/__snapshots__/LogViewer.test.tsx.snap +++ b/frontend/src/components/__snapshots__/LogViewer.test.tsx.snap @@ -2,21 +2,17 @@ exports[`LogViewer does not linkify non http/https urls 1`] = `
1 - + this string: gs://path is a GCS path @@ -27,39 +23,31 @@ exports[`LogViewer does not linkify non http/https urls 1`] = ` exports[`LogViewer linkifies standalone https urls 1`] = `
1 - + this string: - + https://path.com - + is a url @@ -70,39 +58,31 @@ exports[`LogViewer linkifies standalone https urls 1`] = ` exports[`LogViewer linkifies standalone urls 1`] = `
1 - + this string: - + http://path.com - + is a url @@ -113,39 +93,31 @@ exports[`LogViewer linkifies standalone urls 1`] = ` exports[`LogViewer linkifies substring urls 1`] = `
1 - + this string: - + http://path.com - + is a url @@ -156,21 +128,17 @@ exports[`LogViewer linkifies substring urls 1`] = ` exports[`LogViewer renders a multi-line log 1`] = `
1 - + Lorem Ipsum is simply dummy text of the printing and typesetting @@ -181,31 +149,19 @@ exports[`LogViewer renders a multi-line log 1`] = ` exports[`LogViewer renders a row with error 1`] = `
1 - + line1 with error @@ -216,31 +172,19 @@ exports[`LogViewer renders a row with error 1`] = ` exports[`LogViewer renders a row with error word as substring 1`] = `
1 - + line1 with errorWord @@ -251,21 +195,17 @@ exports[`LogViewer renders a row with error word as substring 1`] = ` exports[`LogViewer renders a row with given index as line number 1`] = `
1 - + line1 @@ -276,31 +216,19 @@ exports[`LogViewer renders a row with given index as line number 1`] = ` exports[`LogViewer renders a row with upper case error 1`] = `
1 - + line1 with ERROR @@ -311,31 +239,19 @@ exports[`LogViewer renders a row with upper case error 1`] = ` exports[`LogViewer renders a row with upper case warning 1`] = `
1 - + line1 with WARNING @@ -346,31 +262,19 @@ exports[`LogViewer renders a row with upper case warning 1`] = ` exports[`LogViewer renders a row with warn 1`] = `
1 - + line1 with warn @@ -381,31 +285,19 @@ exports[`LogViewer renders a row with warn 1`] = ` exports[`LogViewer renders a row with warning 1`] = `
1 - + line1 with warning @@ -416,31 +308,19 @@ exports[`LogViewer renders a row with warning 1`] = ` exports[`LogViewer renders a row with warning word as substring 1`] = `
1 - + line1 with warning:something @@ -462,21 +342,17 @@ exports[`LogViewer renders an empty container when no logs passed 1`] = ` exports[`LogViewer renders one log line 1`] = `
1 - + first line @@ -487,21 +363,17 @@ exports[`LogViewer renders one log line 1`] = ` exports[`LogViewer renders one long line without breaking 1`] = `
1 - + Lorem Ipsum is simply dummy text of the printing and typesettingindustry. Lorem Ipsum has been the industry's standard dummy text eversince the 1500s, when an unknown printer took a galley of type andscrambled it to make a type specimen book. It has survived not only fivecenturies, but also the leap into electronic typesetting, remainingessentially unchanged. It was popularised in the 1960s with the releaseof Letraset sheets containing Lorem Ipsum passages, and more recentlywith desktop publishing software like Aldus PageMaker including versionsof Lorem Ipsum. @@ -512,21 +384,17 @@ exports[`LogViewer renders one long line without breaking 1`] = ` exports[`LogViewer renders two log lines 1`] = `
1 - + first line diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a1ec2da63b1..3e642711fb5 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -21,7 +21,8 @@ "strictBindCallApply": true, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, - "noUnusedLocals": true + "noUnusedLocals": true, + "skipLibCheck": true }, "exclude": [ "__mocks__",