diff --git a/superset/assets/.babelrc b/superset/assets/.babelrc index 761abe96e..acf603ce4 100644 --- a/superset/assets/.babelrc +++ b/superset/assets/.babelrc @@ -16,16 +16,19 @@ * specific language governing permissions and limitations * under the License. */ - { +{ "sourceMaps": true, "retainLines": true, - "presets" : ["airbnb", "@babel/preset-react", "@babel/preset-env"], - "plugins": ["lodash", "@babel/plugin-syntax-dynamic-import", "react-hot-loader/babel"], + "presets": ["airbnb", "@babel/preset-react", "@babel/preset-env"], + "plugins": [ + "lodash", + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-proposal-class-properties", + "react-hot-loader/babel" + ], "env": { "test": { - "plugins": [ - "babel-plugin-dynamic-import-node" - ] + "plugins": ["babel-plugin-dynamic-import-node"] } } } diff --git a/superset/assets/jest.config.js b/superset/assets/jest.config.js index 5c3132461..19b984bed 100644 --- a/superset/assets/jest.config.js +++ b/superset/assets/jest.config.js @@ -32,4 +32,11 @@ module.exports = { '^.+\\.tsx?$': 'ts-jest', }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + globals: { + 'ts-jest': { + diagnostics: { + warnOnly: true, + }, + }, + }, }; diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json index 66fcd3e18..832355bca 100644 --- a/superset/assets/package-lock.json +++ b/superset/assets/package-lock.json @@ -435,6 +435,164 @@ } } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.7.4.tgz", + "integrity": "sha512-l+OnKACG4uiDHQ/aJT8dwpR+LhCJALxL0mJ6nzjB25e5IPwqV1VOsY7ah6UB1DG+VOXAIMtuC54rFJGiHkxjgA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-member-expression-to-functions": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4" + }, + "dependencies": { + "@babel/generator": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.7.4.tgz", + "integrity": "sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.7.4.tgz", + "integrity": "sha512-AnkGIdiBhEuiwdoMnKm7jfPfqItZhgRaZfMg1XX3bS25INOnLPjPG1Ppnajh8eqgt5kPJnfqrRHqFqmjKDZLzQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.7.4", + "@babel/template": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.7.4.tgz", + "integrity": "sha512-QTGKEdCkjgzgfJ3bAyRwF4yyT3pg+vDgan8DSivq1eS0gwi+KGKE5x8kRcbeFTb/673mkO5SN1IZfmCfA5o+EA==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.7.4.tgz", + "integrity": "sha512-9KcA1X2E3OjXl/ykfMMInBK+uVdfIVakVe7W7Lg3wfXUNyS3Q1HWLFRwZIjhqiCGbslummPDnmb7vIekS0C1vw==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.7.4.tgz", + "integrity": "sha512-VB7gWZ2fDkSuqW6b1AKXkJWO5NyNI3bFL/kK79/30moK57blr6NbH8xcl2XcKCwOmJosftWunZqfO84IGq3ZZg==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-replace-supers": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.7.4.tgz", + "integrity": "sha512-pP0tfgg9hsZWo5ZboYGuBn/bbYT/hdLPVSS4NMmiRJdwWhP0IznPwN9AE1JwyGsjSPLC364I0Qh5p+EPkGPNpg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.7.4", + "@babel/helper-optimise-call-expression": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.7.4.tgz", + "integrity": "sha512-guAg1SXFcVr04Guk9eq0S4/rWS++sbmyqosJzVs8+1fH5NI+ZcmkaSkc7dmtAFbHFva6yRJnjW3yAcGxjueDug==", + "dev": true, + "requires": { + "@babel/types": "^7.7.4" + } + }, + "@babel/parser": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.4.tgz", + "integrity": "sha512-jIwvLO0zCL+O/LmEJQjWA75MQTWwx3c3u2JOTDK5D3/9egrWRRA0/0hk9XXywYnXZVVpzrBYeIQTmhwUaePI9g==", + "dev": true + }, + "@babel/template": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", + "integrity": "sha512-qUzihgVPguAzXCK7WXw8pqs6cEwi54s3E+HrejlkuWO6ivMKx9hZl3Y2fSXp9i5HgyWmj7RKP+ulaYnKM4yYxw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4" + } + }, + "@babel/traverse": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz", + "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.7.4", + "@babel/helper-function-name": "^7.7.4", + "@babel/helper-split-export-declaration": "^7.7.4", + "@babel/parser": "^7.7.4", + "@babel/types": "^7.7.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + } + } + }, + "@babel/types": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz", + "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "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" + } + }, + "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 + } + } + }, "@babel/helper-define-map": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz", @@ -918,6 +1076,16 @@ "@babel/plugin-syntax-async-generators": "^7.2.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.4.tgz", + "integrity": "sha512-EcuXeV4Hv1X3+Q1TsuOmyyxeTRiSqurGJ26+I/FW1WbymmRRapVORm6x1Zl3iDIHyRxEs+VXWp6qnlcfcJSbbw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.7.4", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-proposal-dynamic-import": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", @@ -9237,8 +9405,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "deep-equal": { "version": "1.0.1", @@ -20880,6 +21047,11 @@ "refractor": "^2.4.1" } }, + "react-table": { + "version": "7.0.0-beta.26", + "resolved": "https://registry.npmjs.org/react-table/-/react-table-7.0.0-beta.26.tgz", + "integrity": "sha512-Pw/1T9kiAjV1cIf6K6bQV6yNQc3O7XUGin1RcSR1xFKw0RNGC5vl1VDPZrNep1BXDsbR6o8O63X45HFNVg6HzA==" + }, "react-test-renderer": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.9.0.tgz", @@ -22019,6 +22191,26 @@ "integrity": "sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==", "dev": true }, + "serialize-query-params": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-0.1.4.tgz", + "integrity": "sha512-d3GHKPAOBULhCMg+jM687vRIMnTXMo8M0lHUOVeFxSGYvfmNlksiOpLyb0orhXPhhFCvZvt+SwC2iPRVIhKS/g==", + "requires": { + "query-string": "^5.0.0" + }, + "dependencies": { + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + } + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -23761,9 +23953,9 @@ } }, "ts-loader": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.3.1.tgz", - "integrity": "sha512-fDDgpBH3SR8xlt2MasLdz3Yy611PQ/UY/KGyo7TgXhTRU/6sS8uGG0nJYnU1OdFBNKcoYbId1UTNaAOUn+i41g==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.4.5.tgz", + "integrity": "sha512-XYsjfnRQCBum9AMRZpk2rTYSVpdZBpZK+kDh0TeT3kxmQNBDVIeUjdPjY5RZry4eIAb8XHc4gYSUiUWPYvzSRw==", "dev": true, "requires": { "chalk": "^2.3.0", @@ -23782,51 +23974,10 @@ "color-convert": "^1.9.0" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "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 - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "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", @@ -23835,274 +23986,26 @@ } }, "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", + "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", + "memory-fs": "^0.5.0", "tapable": "^1.0.0" } }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "http://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "http://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - } - }, - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "dev": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" } }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -24113,37 +24016,38 @@ } }, "tapable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", - "integrity": "sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true } } }, "tslib": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, "tslint": { - "version": "5.11.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", - "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", "dev": true, "requires": { - "babel-code-frame": "^6.22.0", + "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", "commander": "^2.12.1", - "diff": "^3.2.0", + "diff": "^4.0.1", "glob": "^7.1.1", - "js-yaml": "^3.7.0", + "js-yaml": "^3.13.1", "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.8.0", - "tsutils": "^2.27.2" + "tsutils": "^2.29.0" }, "dependencies": { "ansi-styles": { @@ -24156,9 +24060,9 @@ } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "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", @@ -24166,6 +24070,12 @@ "supports-color": "^5.3.0" } }, + "diff": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", + "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -24178,12 +24088,23 @@ } }, "tslint-react": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/tslint-react/-/tslint-react-3.6.0.tgz", - "integrity": "sha512-AIv1QcsSnj7e9pFir6cJ6vIncTqxfqeFF3Lzh8SuuBljueYzEAtByuB6zMaD27BL0xhMEqsZ9s5eHuCONydjBw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tslint-react/-/tslint-react-4.1.0.tgz", + "integrity": "sha512-Y7CbFn09X7Mpg6rc7t/WPbmjx9xPI8p1RsQyiGCLWgDR6sh3+IBSlT+bEkc0PSZcWwClOkqq2wPsID8Vep6szQ==", "dev": true, "requires": { - "tsutils": "^2.13.1" + "tsutils": "^3.9.1" + }, + "dependencies": { + "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" + } + } } }, "tsutils": { @@ -24586,6 +24507,14 @@ "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true }, + "use-query-params": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-0.4.5.tgz", + "integrity": "sha512-HeSgLvEj26pkNRGeAIq+uTo6Z22iaAqDMosq+Be5lab4v57gwVIUKsS3iZ1BBgsUbLEKKpoqcVvqd9MUg+lkIw==", + "requires": { + "serialize-query-params": "^0.1.4" + } + }, "util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", diff --git a/superset/assets/package.json b/superset/assets/package.json index 2add58265..0312de2b4 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -136,6 +136,7 @@ "react-split": "^2.0.4", "react-sticky": "^6.0.2", "react-syntax-highlighter": "^7.0.4", + "react-table": "^7.0.0-beta.26", "react-transition-group": "^2.5.3", "react-virtualized": "9.19.1", "react-virtualized-select": "^3.1.3", @@ -146,12 +147,14 @@ "redux-undo": "^1.0.0-beta9-9-7", "regenerator-runtime": "^0.13.3", "shortid": "^2.2.6", - "urijs": "^1.18.10" + "urijs": "^1.18.10", + "use-query-params": "^0.4.5" }, "devDependencies": { "@babel/cli": "^7.5.5", "@babel/core": "^7.5.5", "@babel/node": "^7.5.5", + "@babel/plugin-proposal-class-properties": "^7.7.4", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/preset-env": "^7.5.5", "@babel/preset-react": "^7.0.0", @@ -208,9 +211,10 @@ "thread-loader": "^1.2.0", "transform-loader": "^0.2.3", "ts-jest": "^24.0.2", - "ts-loader": "^5.2.0", - "tslint": "^5.11.0", - "tslint-react": "^3.6.0", + "ts-loader": "^5.4.5", + "tslib": "^1.10.0", + "tslint": "^5.20.1", + "tslint-react": "^4.1.0", "typescript": "^3.5.3", "url-loader": "^1.0.1", "webpack": "^4.19.0", diff --git a/superset/assets/spec/javascripts/components/ListView/ListView_spec.jsx b/superset/assets/spec/javascripts/components/ListView/ListView_spec.jsx new file mode 100644 index 000000000..79ace2c4b --- /dev/null +++ b/superset/assets/spec/javascripts/components/ListView/ListView_spec.jsx @@ -0,0 +1,175 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { MenuItem, Pagination } from 'react-bootstrap'; + +import ListView from 'src/components/ListView/ListView'; + +describe('ListView', () => { + const mockedProps = { + title: 'Data Table', + columns: [ + { + accessor: 'id', + Header: 'ID', + sortable: true, + }, + { + accessor: 'name', + Header: 'Name', + filterable: true, + }, + ], + data: [ + { id: 1, name: 'data 1' }, + { id: 2, name: 'data 2' }, + ], + count: 2, + pageSize: 1, + fetchData: jest.fn(() => []), + loading: false, + filterTypes: { + id: [], + name: [{ name: 'sw', label: 'Starts With' }], + }, + }; + const wrapper = mount(); + + afterEach(() => { + mockedProps.fetchData.mockClear(); + }); + + it('calls fetchData on mount', () => { + expect(wrapper.find(ListView)).toHaveLength(1); + expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "filters": Object {}, + "pageIndex": 0, + "pageSize": 1, + "sortBy": Array [], + }, + ] + `); + }); + + it('calls fetchData on sort', () => { + wrapper + .find('[data-test="sort-header"]') + .first() + .simulate('click'); + expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "filters": Object {}, + "pageIndex": 0, + "pageSize": 1, + "sortBy": Array [ + Object { + "desc": false, + "id": "id", + }, + ], + }, + ] + `); + }); + + it('calls fetchData on filter', () => { + act(() => { + wrapper + .find('.dropdown-toggle') + .children('button') + .props() + .onClick(); + + wrapper + .find(MenuItem) + .props() + .onSelect({ id: 'name', Header: 'name' }); + }); + wrapper.update(); + + act(() => { + wrapper.find('.filter-inputs input[type="text"]').prop('onChange')({ + currentTarget: { value: 'foo' }, + }); + }); + wrapper.update(); + + act(() => { + wrapper + .find('[data-test="apply-filters"]') + .last() + .prop('onClick')(); + }); + wrapper.update(); + + expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "filters": Object { + "name": Object { + "filterId": "sw", + "filterValue": "foo", + }, + }, + "pageIndex": 0, + "pageSize": 1, + "sortBy": Array [ + Object { + "desc": false, + "id": "id", + }, + ], + }, + ] + `); + }); + + it('calls fetchData on page change', () => { + act(() => { + wrapper.find(Pagination).prop('onSelect')(2); + }); + wrapper.update(); + + expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "filters": Object { + "name": Object { + "filterId": "sw", + "filterValue": "foo", + }, + }, + "pageIndex": 1, + "pageSize": 1, + "sortBy": Array [ + Object { + "desc": false, + "id": "id", + }, + ], + }, + ] + `); + }); +}); diff --git a/superset/assets/spec/javascripts/explore/components/ExploreChartHeader_spec.jsx b/superset/assets/spec/javascripts/explore/components/ExploreChartHeader_spec.jsx index ec7278ffd..72e03c5f2 100644 --- a/superset/assets/spec/javascripts/explore/components/ExploreChartHeader_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/ExploreChartHeader_spec.jsx @@ -68,7 +68,7 @@ describe('ExploreChartHeader', () => { it('should updateChartTitleOrSaveSlice for existed slice', () => { const newTitle = 'New Chart Title'; wrapper.instance().updateChartTitleOrSaveSlice(newTitle); - expect(stub.call.length).toEqual(1); + expect(stub.call).toHaveLength(1); expect(stub).toHaveBeenCalledWith(mockProps.slice.form_data, { action: 'overwrite', slice_name: newTitle, @@ -79,7 +79,7 @@ describe('ExploreChartHeader', () => { const newTitle = 'New Chart Title'; wrapper.setProps({ slice: undefined }); wrapper.instance().updateChartTitleOrSaveSlice(newTitle); - expect(stub.call.length).toEqual(1); + expect(stub.call).toHaveLength(1); expect(stub).toHaveBeenCalledWith(mockProps.form_data, { action: 'saveas', slice_name: newTitle, diff --git a/superset/assets/spec/javascripts/views/dashboardList/DashboardList_spec.jsx b/superset/assets/spec/javascripts/views/dashboardList/DashboardList_spec.jsx new file mode 100644 index 000000000..51026e648 --- /dev/null +++ b/superset/assets/spec/javascripts/views/dashboardList/DashboardList_spec.jsx @@ -0,0 +1,82 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mount } from 'enzyme'; +import thunk from 'redux-thunk'; +import configureStore from 'redux-mock-store'; +import fetchMock from 'fetch-mock'; + +import DashboardList from 'src/views/dashboardList/DashboardList'; +import ListView from 'src/components/ListView/ListView'; + +// store needed for withToasts(DashboardTable) +const mockStore = configureStore([thunk]); +const store = mockStore({}); + +const dashboardsInfoEndpoint = 'glob:*/api/v1/dashboard/_info*'; +const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*'; + +const mockDashboards = [...new Array(3)].map((_, i) => ({ + id: i, + url: 'url', + dashboard_title: `title ${i}`, + changed_by_name: 'user', + changed_by_url: 'changed_by_url', + changed_by_fk: 1, + published: true, + changed_on: new Date().toISOString(), +})); + +fetchMock.get(dashboardsInfoEndpoint, { + permissions: ['can_list', 'can_edit'], + filters: [], +}); +fetchMock.get(dashboardsEndpoint, { + result: mockDashboards, + dashboard_count: 3, +}); + +describe('DashboardList', () => { + const mockedProps = {}; + const wrapper = mount(, { + context: { store }, + }); + + it('renders', () => { + expect(wrapper.find(DashboardList)).toHaveLength(1); + }); + + it('renders a ListView', () => { + expect(wrapper.find(ListView)).toHaveLength(1); + }); + + it('fetches info', () => { + const callsI = fetchMock.calls(/dashboard\/_info/); + expect(callsI).toHaveLength(1); + }); + + it('fetches data', () => { + wrapper.update(); + const callsD = fetchMock.calls(/dashboard\/\?q/); + expect(callsD).toHaveLength(1); + expect(callsD[0][0]).toMatchInlineSnapshot( + `"/http//localhost/api/v1/dashboard/?q={%22order_column%22:%22changed_on%22,%22order_direction%22:%22desc%22,%22page%22:0,%22page_size%22:25}"`, + ); + }); +}); diff --git a/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx b/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx index 42f2dd7d7..a51969513 100644 --- a/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx +++ b/superset/assets/spec/javascripts/welcome/DashboardTable_spec.jsx @@ -21,16 +21,16 @@ import { mount } from 'enzyme'; import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; -import { Table } from 'reactable-arc'; +import ListView from 'src/components/ListView/ListView'; import DashboardTable from '../../../src/welcome/DashboardTable'; import Loading from '../../../src/components/Loading'; -// store needed for withToasts(TableLoader) +// store needed for withToasts(DashboardTable) const mockStore = configureStore([thunk]); const store = mockStore({}); -const dashboardsEndpoint = 'glob:*/dashboardasync/api/read*'; +const dashboardsEndpoint = 'glob:*/api/v1/dashboard/*'; const mockDashboards = [{ id: 1, url: 'url', dashboard_title: 'title' }]; fetchMock.get(dashboardsEndpoint, { result: mockDashboards }); @@ -48,7 +48,7 @@ describe('DashboardTable', () => { expect(wrapper.find(Loading)).toHaveLength(1); }); - it('fetches dashboards and renders a Table', done => { + it('fetches dashboards and renders a ListView', done => { const wrapper = setup(); setTimeout(() => { @@ -56,7 +56,7 @@ describe('DashboardTable', () => { // there's a delay between response and updating state, so manually set it // rather than adding a timeout which could introduce flakiness wrapper.setState({ dashaboards: mockDashboards }); - expect(wrapper.find(Table)).toHaveLength(1); + expect(wrapper.find(ListView)).toHaveLength(1); done(); }); }); diff --git a/superset/assets/src/components/ListView/ListView.tsx b/superset/assets/src/components/ListView/ListView.tsx new file mode 100644 index 000000000..f236d74c5 --- /dev/null +++ b/superset/assets/src/components/ListView/ListView.tsx @@ -0,0 +1,254 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { t } from '@superset-ui/translation'; +import React, { FunctionComponent, useMemo } from 'react'; +import { + Button, + Col, + DropdownButton, + FormControl, + MenuItem, + Pagination, + Row, + // @ts-ignore +} from 'react-bootstrap'; +import Loading from '../Loading'; +import './ListViewStyles.less'; +import TableCollection from './TableCollection'; +import { FetchDataConfig, FilterToggle, FilterType, FilterTypeMap, SortColumn } from './types'; +import { convertFilters, removeFromList, useListViewState } from './utils'; + +interface Props { + columns: any[]; + data: any[]; + count: number; + pageSize: number; + fetchData: (conf: FetchDataConfig) => any; + loading: boolean; + className?: string; + title?: string; + initialSort?: SortColumn[]; + filterTypes?: FilterTypeMap; +} + +const ListView: FunctionComponent = ({ + columns, + data, + count, + pageSize: initialPageSize, + fetchData, + loading, + initialSort = [], + className = '', + title = '', + filterTypes = {}, +}) => { + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + canPreviousPage, + canNextPage, + pageCount = 1, + gotoPage, + setAllFilters, + setFilterToggles, + updateFilterToggle, + applyFilters, + filtersApplied, + state: { pageIndex, pageSize, filterToggles }, + } = useListViewState({ + columns, + count, + data, + fetchData, + initialPageSize, + initialSort, + }); + const filterableColumns = useMemo(() => columns.filter((c) => c.filterable), [columns]); + const filterable = Boolean(columns.length); + + const removeFilterAndApply = (index: number) => { + const updated = removeFromList(filterToggles, index); + setFilterToggles(updated); + setAllFilters(convertFilters(updated)); + }; + + if (loading) { + return ; + } + + return ( +
+ {title && filterable && ( +
+ + +

{t(title)}

+ + {filterable && ( + +
+ + + {' '}{t('Filter List')} + + )} + id={'filter-picker'} + > + {filterableColumns + .map(({ id, accessor, Header }) => ({ + Header, + id: id || accessor, + })) + .map((ft: FilterToggle) => ( + { + setFilterToggles([...filterToggles, fltr]); + } + } + > + {ft.Header} + + ))} + +
+ + )} +
+
+ {filterToggles.map((ft, i) => ( +
+ + + {ft.Header} + + + ) => + updateFilterToggle(i, { filterId: e.currentTarget.value }) + } + > + {filterTypes[ft.id] && filterTypes[ft.id].map( + ({ name, operator }: FilterType) => ( + + ), + )} + + + + + ) => + updateFilterToggle(i, { + filterValue: e.currentTarget.value, + }) + } + /> + + +
removeFilterAndApply(i)} + > + +
+ +
+
+
+ ))} + {filterToggles.length > 0 && ( + <> + + + + + + +
+ + )} +
+ ) + } +
+ +
+
+ 1} + next={canNextPage} + last={pageIndex < pageCount - 2} + items={pageCount} + activePage={pageIndex + 1} + ellipsis={true} + boundaryLinks={true} + maxButtons={5} + onSelect={(p: number) => gotoPage(p - 1)} + /> + + {t('showing')}{' '} + + {pageSize * pageIndex + (rows.length && 1)}- + {pageSize * pageIndex + rows.length} + {' '} + {t('of')} {count} + +
+
+ ); +}; + +export default ListView; diff --git a/superset/assets/src/components/ListView/ListViewStyles.less b/superset/assets/src/components/ListView/ListViewStyles.less new file mode 100644 index 000000000..e163a4b6c --- /dev/null +++ b/superset/assets/src/components/ListView/ListViewStyles.less @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +.superset-list-view { + .filter-dropdown { + margin-top: 20px; + } + + .filter-column { + height: 30px; + padding: 5px; + font-size: 16px; + } + + .filter-close { + height: 30px; + padding: 5px; + + i { + font-size: 20px; + } + } + + .table-row-loader { + animation: shimmer 2s infinite; + background: linear-gradient( + to right, + #f6f7f8 0%, + #edeef1 20%, + #f6f7f8 40%, + #f6f7f8 100% + ); + background-size: 1000px 100%; + + span { + visibility: hidden; + } + } +} + +@keyframes shimmer { + 0% { + background-position: -1000px 0; + } + + 100% { + background-position: 1000px 0; + } +} diff --git a/superset/assets/src/components/ListView/TableCollection.tsx b/superset/assets/src/components/ListView/TableCollection.tsx new file mode 100644 index 000000000..f914b2bb9 --- /dev/null +++ b/superset/assets/src/components/ListView/TableCollection.tsx @@ -0,0 +1,90 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { Cell, HeaderGroup, Row } from 'react-table'; + +interface Props { + getTableProps: (userProps?: any) => any; + getTableBodyProps: (userProps?: any) => any; + prepareRow: (row: Row) => any; + headerGroups: Array>; + rows: Array>; + loading: boolean; +} +/* tslint:disable:jsx-key */ +export default function TableCollection({ + getTableProps, + getTableBodyProps, + prepareRow, + headerGroups, + rows, + loading, +}: Props) { + return ( + + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column: any) => ( + + ))} + + ))} + + + {rows.map((row) => { + prepareRow(row); + const loadingProps = loading ? { className: 'table-row-loader' } : {}; + return ( + row.setState && row.setState({ hover: true })} + onMouseLeave={() => row.setState && row.setState({ hover: false })} + > + {row.cells.map((cell: Cell) => { + const columnCellProps = cell.column.cellProps || {}; + + return ( + + ); + })} + + ); + })} + +
+ {column.render('Header')} + {' '} + {column.sortable && ( + + )} +
+ {cell.render('Cell')} +
+ ); +} diff --git a/superset/assets/src/components/ListView/types.ts b/superset/assets/src/components/ListView/types.ts new file mode 100644 index 000000000..0151d4515 --- /dev/null +++ b/superset/assets/src/components/ListView/types.ts @@ -0,0 +1,56 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +export interface SortColumn { + id: string; + desc: boolean; +} + +export type SortColumns = SortColumn[]; + +export interface Filter { + filterId: number; + filterValue: string; +} + +export interface FilterType { + name: string; + operator: any; +} + +export interface FilterTypeMap { + [columnId: string]: FilterType[]; +} + +interface FilterMap { + [columnId: string]: Filter; +} + +export interface FetchDataConfig { + pageIndex: number; + pageSize: number; + sortBy: SortColumns; + filters: FilterMap; +} + +export interface FilterToggle { + id: string; + Header: string; + filterId?: number; + filterValue?: string; +} diff --git a/superset/assets/src/components/ListView/utils.ts b/superset/assets/src/components/ListView/utils.ts new file mode 100644 index 000000000..d62de9407 --- /dev/null +++ b/superset/assets/src/components/ListView/utils.ts @@ -0,0 +1,172 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { useEffect, useState } from 'react'; +import { + useFilters, + usePagination, + useRowState, + useSortBy, + useTable, +} from 'react-table'; + +import { + JsonParam, + NumberParam, + StringParam, + useQueryParams, +} from 'use-query-params'; + +import { FetchDataConfig, FilterToggle, SortColumn } from './types'; + +// removes element from a list, returns new list +export function removeFromList(list: any[], index: number): any[] { + return list.filter((_, i) => index !== i); +} + +// apply update to elements of object list, returns new list +function updateInList(list: any[], index: number, update: any): any[] { + const element = list.find((_, i) => index === i); + + return [ + ...list.slice(0, index), + { ...element, ...update }, + ...list.slice(index + 1), + ]; +} + +// convert filters from UI objects to data objects +export function convertFilters(fts: FilterToggle[]) { + return fts + .filter((ft: FilterToggle) => ft.filterValue) + .reduce((acc, ft) => { + acc[ft.id] = { + filterId: ft.filterId || 'sw', + filterValue: ft.filterValue, + }; + return acc; + }, {}); +} + +interface UseListViewConfig { + fetchData: (conf: FetchDataConfig) => any; + columns: any[]; + data: any[]; + count: number; + initialPageSize: number; + initialSort?: SortColumn[]; +} + +export function useListViewState({ + fetchData, + columns, + data, + count, + initialPageSize, + initialSort = [], +}: UseListViewConfig) { + const [query, setQuery] = useQueryParams({ + filters: JsonParam, + pageIndex: NumberParam, + sortColumn: StringParam, + sortOrder: StringParam, + }); + + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + canPreviousPage, + canNextPage, + pageCount, + gotoPage, + setAllFilters, + state: { pageIndex, pageSize, sortBy, filters }, + } = useTable( + { + columns, + count, + data, + disableSortRemove: true, + initialState: { + filters: convertFilters(query.filters || []), + pageIndex: query.pageIndex || 0, + pageSize: initialPageSize, + sortBy: + query.sortColumn && query.sortOrder + ? [{ id: query.sortColumn, desc: query.sortOrder === 'desc' }] + : initialSort, + }, + manualFilters: true, + manualPagination: true, + manualSorting: true, + pageCount: Math.ceil(count / initialPageSize), + }, + useFilters, + useSortBy, + usePagination, + useRowState, + ); + + const [filterToggles, setFilterToggles] = useState( + query.filters || [], + ); + + useEffect(() => { + const queryParams: any = { + filters: filterToggles, + pageIndex, + }; + if (sortBy[0]) { + queryParams.sortColumn = sortBy[0].id; + queryParams.sortOrder = sortBy[0].desc ? 'desc' : 'asc'; + } + setQuery(queryParams); + + fetchData({ pageIndex, pageSize, sortBy, filters }); + }, [fetchData, pageIndex, pageSize, sortBy, filters]); + + const filtersApplied = filterToggles.every( + ({ id, filterValue, filterId = 'sw' }) => + id && + filters[id] && + filters[id].filterValue === filterValue && + filters[id].filterId === filterId, + ); + + return { + applyFilters: () => setAllFilters(convertFilters(filterToggles)), + canNextPage, + canPreviousPage, + filtersApplied, + getTableBodyProps, + getTableProps, + gotoPage, + headerGroups, + pageCount, + prepareRow, + rows, + setAllFilters, + setFilterToggles, + state: { pageIndex, pageSize, sortBy, filters, filterToggles }, + updateFilterToggle: (index: number, update: object) => + setFilterToggles(updateInList(filterToggles, index, update)), + }; +} diff --git a/superset/assets/src/types/react-table.d.ts b/superset/assets/src/types/react-table.d.ts new file mode 100644 index 000000000..5a1e72758 --- /dev/null +++ b/superset/assets/src/types/react-table.d.ts @@ -0,0 +1,243 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Type definitions for react-table 7 +// Project: https://github.com/tannerlinsley/react-table#readme +// Definitions by: Adrien Denat +// Artem Berdyshev +// Christian Murphy +// Tai Dupreee +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 3.0 +declare module 'react-table' { + import { Dispatch, ReactNode, SetStateAction } from 'react'; + + export interface Cell { + render: (type: string) => any; + getCellProps: () => { key: string; [k: string]: any }; + column: Column; + row: Row; + state: any; + value: any; + } + + export interface Row { + index: number; + cells: Array>; + getRowProps: () => { key: string; [k: string]: any }; + original: any; + state?: any; + setState?: (state: any) => any; + } + + export interface HeaderColumn { + /** + * This string/function is used to build the data model for your column. + */ + accessor: A | ((originalRow: D) => string); + Header?: string | ((props: TableInstance) => ReactNode); + Filter?: string | ((props: TableInstance) => ReactNode); + Cell?: string | ((cell: Cell) => ReactNode); + + /** + * This is the unique ID for the column. It is used by reference in things like sorting, grouping, filtering etc. + */ + id?: string | number; + minWidth?: string | number; + maxWidth?: string | number; + width?: string | number; + canSortBy?: boolean; + sortByFn?: (a: any, b: any, desc: boolean) => 0 | 1 | -1; + defaultSortDesc?: boolean; + [key: string]: any; + } + + export interface Column + extends HeaderColumn { + id: string | number; + } + + export type Page = Array>; + + export interface EnhancedColumn + extends Column { + render: (type: string) => any; + getHeaderProps: (userProps?: any) => any; + getSortByToggleProps: (userProps?: any) => any; + sorted: boolean; + sortedDesc: boolean; + sortedIndex: number; + } + + export interface HeaderGroup { + headers: Array>; + getRowProps: (userProps?: any) => any; + getHeaderGroupProps: (userProps?: any) => any; + } + + export interface Hooks { + beforeRender: []; + columns: []; + headerGroups: []; + headers: []; + rows: Array>; + row: []; + renderableRows: []; + getTableProps: []; + getRowProps: []; + getHeaderRowProps: []; + getHeaderProps: []; + getCellProps: []; + } + + export interface TableInstance + extends TableOptions, + UseRowsValues, + UseFiltersValues, + UsePaginationValues, + UseColumnsValues, + UseRowStateValues { + hooks: Hooks; + rows: Array>; + columns: Array>; + getTableProps: (userProps?: any) => any; + getTableBodyProps: (userProps?: any) => any; + getRowProps: (userProps?: any) => any; + prepareRow: (row: Row) => any; + getSelectRowToggleProps: (userProps?: any) => any; + toggleSelectAll: (forcedState: boolean) => any; + state: { [key: string]: any }; + } + + export interface TableOptions { + data: D[]; + columns: Array>; + state?: { [key: string]: any }; + debug?: boolean; + sortByFn?: (a: any, b: any, desc: boolean) => 0 | 1 | -1; + manualSorting?: boolean; + manualFilters?: boolean; + manualPagination?: boolean; + pageCount?: number; + disableSorting?: boolean; + defaultSortDesc?: boolean; + disableMultiSort?: boolean; + count?: number; + disableSortRemove?: boolean; + initialState?: any; + } + + export interface RowsProps { + subRowsKey: string; + } + + export interface FiltersProps { + filterFn: () => void; + manualFilters: boolean; + disableFilters: boolean; + setFilter: (columnId: string, filter: string) => any; + setAllFilters: (filterObj: any) => any; + } + + export interface UsePaginationValues { + nextPage: () => any; + previousPage: () => any; + setPageSize: (size: number) => any; + gotoPage: (page: number) => any; + canPreviousPage: boolean; + canNextPage: boolean; + page: Page; + pageOptions: []; + } + + export interface UseRowsValues { + rows: Array>; + } + + export interface UseColumnsValues { + columns: Array>; + headerGroups: Array>; + headers: Array>; + } + + export interface UseFiltersValues { + setFilter: (columnId: string, filter: string) => any; + setAllFilters: (filterObj: any) => any; + } + + export interface UseRowStateValues { + setRowState: (rowPath: string[], updater: (state: any) => any) => any; + } + + export function useTable( + props: TableOptions, + ...plugins: any[] + ): TableInstance; + + export function useColumns( + props: TableOptions, + ): TableOptions & UseColumnsValues; + + export function useRows( + props: TableOptions, + ): TableOptions & UseRowsValues; + + export function useFilters( + props: TableOptions, + ): TableOptions & { + rows: Array>; + }; + + export function useSortBy( + props: TableOptions, + ): TableOptions & { + rows: Array>; + }; + + export function useGroupBy( + props: TableOptions, + ): TableOptions & { rows: Array> }; + + export function usePagination( + props: TableOptions, + ): UsePaginationValues; + + export function useRowState(props: TableOptions): UseRowStateValues; + + export function useFlexLayout(props: TableOptions): TableOptions; + + export function useExpanded( + props: TableOptions, + ): TableOptions & { + toggleExpandedByPath: () => any; + expandedDepth: []; + rows: []; + }; + + export function useTableState( + initialState?: any, + overriddenState?: any, + options?: { + reducer?: (oldState: any, newState: any, type: string) => any; + useState?: [any, Dispatch>]; + }, + ): any; + + export const actions: any; +} diff --git a/superset/assets/src/views/dashboardList/DashboardList.less b/superset/assets/src/views/dashboardList/DashboardList.less new file mode 100644 index 000000000..fd58fb8f1 --- /dev/null +++ b/superset/assets/src/views/dashboardList/DashboardList.less @@ -0,0 +1,27 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +.dashboard-list-view { + .actions { + font-size: 20px; + } + + .action-button { + margin: 0 8px; + } +} diff --git a/superset/assets/src/views/dashboardList/DashboardList.tsx b/superset/assets/src/views/dashboardList/DashboardList.tsx new file mode 100644 index 000000000..0bc2823ae --- /dev/null +++ b/superset/assets/src/views/dashboardList/DashboardList.tsx @@ -0,0 +1,294 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SupersetClient } from '@superset-ui/connection'; +import { t } from '@superset-ui/translation'; +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +// @ts-ignore +import { Button, Modal, Panel } from 'react-bootstrap'; +import ListView from 'src/components/ListView/ListView'; +import { FilterTypeMap } from 'src/components/ListView/types'; +import { FetchDataConfig } from 'src/components/ListView/types'; +import withToasts from 'src/messageToasts/enhancers/withToasts'; + +import './DashboardList.less'; + +const PAGE_SIZE = 25; + +interface Props { + addDangerToast: (msg: string) => void; +} + +interface State { + dashboards: any[]; + dashboardCount: number; + loading: boolean; + showDeleteModal: boolean; + deleteCandidate: any; + filterTypes: FilterTypeMap; + permissions: string[]; + labelColumns: { [key: string]: string }; +} +class DashboardList extends React.PureComponent { + + get canEdit() { + return this.hasPerm('can_edit'); + } + + get canDelete() { + return this.hasPerm('can_delete'); + } + + public static propTypes = { + addDangerToast: PropTypes.func.isRequired, + }; + + public state: State = { + dashboardCount: 0, + dashboards: [], + deleteCandidate: {}, + filterTypes: {}, + labelColumns: {}, + loading: false, + permissions: [], + showDeleteModal: false, + }; + + public columns: any = []; + + public initialSort = [{ id: 'changed_on', desc: true }]; + + constructor(props: Props) { + super(props); + this.setColumns(); + } + + public setColumns = () => { + this.columns = [ + { + Cell: ({ + row: { + original: { url, dashboard_title }, + }, + }: any) => {dashboard_title}, + Header: this.state.labelColumns.dashboard_title || '', + accessor: 'dashboard_title', + filterable: true, + sortable: true, + }, + { + Cell: ({ + row: { + original: { changed_by_name, changed_by_url }, + }, + }: any) => {changed_by_name}, + Header: this.state.labelColumns.changed_by_name || '', + accessor: 'changed_by_fk', + sortable: true, + }, + { + Cell: ({ + row: { + original: { published }, + }, + }: any) => ( + {published ? : ''} + ), + Header: this.state.labelColumns.published || '', + accessor: 'published', + sortable: true, + }, + { + Cell: ({ + row: { + original: { changed_on }, + }, + }: any) => ( + {moment(changed_on).fromNow()} + ), + Header: this.state.labelColumns.changed_on || '', + accessor: 'changed_on', + sortable: true, + }, + { + Cell: ({ row: { state, original } }: any) => { + const handleDelete = () => this.handleDashboardDeleteConfirm(original); + const handleEdit = () => this.handleDashboardEdit(original); + if (!this.canEdit && !this.canDelete) { + return null; + } + + return ( + + {this.canDelete && ( + + + + )} + {this.canEdit && ( + + + + )} + + ); + }, + Header: 'Actions', + id: 'actions', + }, + ]; + } + + public hasPerm = (perm: string) => { + if (!this.state.permissions.length) { + return false; + } + + return Boolean(this.state.permissions.find((p) => p === perm)); + } + + public handleDashboardEdit = ({ id }: { id: number }) => { + window.location.assign(`/dashboard/edit/${id}`); + } + + public handleDashboardDeleteConfirm = (dashboard: any) => { + this.setState({ + deleteCandidate: dashboard, + showDeleteModal: true, + }); + } + + public handleDashboardDelete = () => { + const { id, title } = this.state.deleteCandidate; + SupersetClient.delete({ + endpoint: `/api/v1/dashboard/${id}`, + }).then( + (resp) => { + const dashboards = this.state.dashboards.filter((d) => d.id !== id); + this.setState({ + dashboards, + deleteCandidate: {}, + showDeleteModal: false, + }); + }, + (err: any) => { + this.props.addDangerToast(t('There was an issue deleting') + `${title}`); + this.setState({ showDeleteModal: false, deleteCandidate: {} }); + }, + ); + } + + public toggleModal = () => { + this.setState({ showDeleteModal: !this.state.showDeleteModal }); + } + + public fetchData = ({ + pageIndex, + pageSize, + sortBy, + filters, + }: FetchDataConfig) => { + this.setState({ loading: true }); + const filterExps = Object.keys(filters).map((fk) => ({ + col: fk, + opr: filters[fk].filterId, + value: filters[fk].filterValue, + })); + + const queryParams = JSON.stringify({ + order_column: sortBy[0].id, + order_direction: sortBy[0].desc ? 'desc' : 'asc', + page: pageIndex, + page_size: pageSize, + ...(filterExps.length ? { filters: filterExps } : {}), + }); + + return SupersetClient.get({ + endpoint: `/api/v1/dashboard/?q=${queryParams}`, + }) + .then(({ json = {} }) => { + this.setState({ dashboards: json.result, dashboardCount: json.count, labelColumns: json.label_columns }); + }) + .catch(() => { + this.props.addDangerToast( + t('An error occurred while fetching Dashboards'), + ); + }) + .finally(() => { + this.setColumns(); + this.setState({ loading: false }); + }); + } + + public componentDidMount() { + SupersetClient.get({ + endpoint: `/api/v1/dashboard/_info`, + }) + .then(({ json = {} }) => { + this.setState({ filterTypes: json.filters, permissions: json.permissions }); + }); + } + + public render() { + const { dashboards, dashboardCount, loading, filterTypes } = this.state; + return ( +
+ + + + + + + + {t('Are you sure you want to delete')}{' '} + {this.state.deleteCandidate.dashboard_title}? + + + + + + +
+ ); + } +} + +export default withToasts(DashboardList); diff --git a/superset/assets/src/welcome/App.jsx b/superset/assets/src/welcome/App.jsx index 5e12bc1af..77fe6aa8b 100644 --- a/superset/assets/src/welcome/App.jsx +++ b/superset/assets/src/welcome/App.jsx @@ -23,11 +23,13 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; +import Menu from 'src/components/Menu/Menu'; +import DashboardList from 'src/views/dashboardList/DashboardList'; + import messageToastReducer from '../messageToasts/reducers'; import { initEnhancer } from '../reduxUtils'; import setupApp from '../setup/setupApp'; import Welcome from './Welcome'; -import Menu from '../components/Menu/Menu'; import ToastPresenter from '../messageToasts/containers/ToastPresenter'; setupApp(); @@ -50,11 +52,14 @@ const App = () => ( - + - + + + + ); diff --git a/superset/assets/src/welcome/DashboardTable.jsx b/superset/assets/src/welcome/DashboardTable.jsx index 079fd0e87..c8fbea24c 100644 --- a/superset/assets/src/welcome/DashboardTable.jsx +++ b/superset/assets/src/welcome/DashboardTable.jsx @@ -18,34 +18,106 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { Table, Tr, Td, unsafe } from 'reactable-arc'; -import { SupersetClient } from '@superset-ui/connection'; import { t } from '@superset-ui/translation'; +import { SupersetClient } from '@superset-ui/connection'; +import moment from 'moment'; +import { debounce } from 'lodash'; +import ListView from 'src/components/ListView/ListView'; +import withToasts from 'src/messageToasts/enhancers/withToasts'; -import withToasts from '../messageToasts/enhancers/withToasts'; -import Loading from '../components/Loading'; -import '../../stylesheets/reactable-pagination.less'; - -const propTypes = { - search: PropTypes.string, - addDangerToast: PropTypes.func.isRequired, -}; +const PAGE_SIZE = 25; class DashboardTable extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - dashboards: null, - }; + static propTypes = { + addDangerToast: PropTypes.func.isRequired, + search: PropTypes.string, + }; + + state = { + dashboards: [], + dashboard_count: 0, + loading: false, + }; + + componentDidUpdate(prevProps) { + if (prevProps.search !== this.props.search) { + this.fetchDataDebounced({ + pageSize: PAGE_SIZE, + pageIndex: 0, + sortBy: this.initialSort, + filters: {}, + }); + } } - componentDidMount() { - SupersetClient.get({ - endpoint: - '/dashboardasync/api/read?_oc_DashboardModelViewAsync=changed_on&_od_DashboardModelViewAsync=desc', + columns = [ + { + accessor: 'dashboard_title', + Header: 'Dashboard', + sortable: true, + Cell: ({ + row: { + original: { url, dashboard_title: dashboardTitle }, + }, + }) => {dashboardTitle}, + }, + { + accessor: 'changed_by_fk', + Header: 'Creator', + sortable: true, + Cell: ({ + row: { + original: { changed_by_name: changedByName, changedByUrl }, + }, + }) => {changedByName}, + }, + { + accessor: 'changed_on', + Header: 'Modified', + sortable: true, + Cell: ({ + row: { + original: { changed_on: changedOn }, + }, + }) => {moment(changedOn).fromNow()}, + }, + ]; + + initialSort = [{ id: 'changed_on', desc: true }]; + + fetchData = ({ pageIndex, pageSize, sortBy, filters }) => { + this.setState({ loading: true }); + const filterExps = Object.keys(filters) + .map(fk => ({ + col: fk, + opr: filters[fk].filterId, + value: filters[fk].filterValue, + })) + .concat( + this.props.search + ? [ + { + col: 'dashboard_title', + opr: 'ct', + value: this.props.search, + }, + ] + : [], + ); + + const queryParams = JSON.stringify({ + order_column: sortBy[0].id, + order_direction: sortBy[0].desc ? 'desc' : 'asc', + page: pageIndex, + page_size: pageSize, + ...(filterExps.length ? { filters: filterExps } : {}), + }); + + return SupersetClient.get({ + endpoint: `/api/v1/dashboard/?q=${queryParams}`, }) .then(({ json }) => { - this.setState({ dashboards: json.result }); + this.setState({ dashboards: json.result, dashboard_count: json.count }); }) .catch(response => { if (response.status === 401) { @@ -59,47 +131,25 @@ class DashboardTable extends React.PureComponent { t('An error occurred while fetching Dashboards'), ); } - }); - } + }) + .finally(() => this.setState({ loading: false })); + }; + + fetchDataDebounced = debounce(this.fetchData, 200); render() { - if (this.state.dashboards !== null) { - return ( - - {this.state.dashboards.map(o => ( - - - - - - ))} -
- {o.dashboard_title} - - {unsafe(o.creator)} - - {unsafe(o.modified)} -
- ); - } - - return ; + return ( + + ); } } -DashboardTable.propTypes = propTypes; - export default withToasts(DashboardTable); diff --git a/superset/assets/src/welcome/Welcome.jsx b/superset/assets/src/welcome/Welcome.jsx index 54e986242..3b7a2f101 100644 --- a/superset/assets/src/welcome/Welcome.jsx +++ b/superset/assets/src/welcome/Welcome.jsx @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Panel, Row, Col, Tabs, Tab, FormControl } from 'react-bootstrap'; import { t } from '@superset-ui/translation'; +import { useQueryParam, StringParam } from 'use-query-params'; import RecentActivity from '../profile/components/RecentActivity'; import Favorites from '../profile/components/Favorites'; import DashboardTable from './DashboardTable'; @@ -28,68 +29,84 @@ const propTypes = { user: PropTypes.object.isRequired, }; -export default class Welcome extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - search: '', - }; - this.onSearchChange = this.onSearchChange.bind(this); - } - onSearchChange(event) { - this.setState({ search: event.target.value }); - } - render() { - return ( -
- - - - - -

{t('Dashboards')}

- - - - -
-
- -
-
- - - - -

{t('Recently Viewed')}

- -
-
- -
-
- - - - -

{t('Favorites')}

- -
-
- -
-
-
-
- ); - } +function useSyncQueryState(queryParam, queryParamType, defaultState) { + const [queryState, setQueryState] = useQueryParam(queryParam, queryParamType); + const [state, setState] = useState(queryState || defaultState); + + const setQueryStateAndState = val => { + setQueryState(val); + setState(val); + }; + + return [state, setQueryStateAndState]; +} + +export default function Welcome({ user }) { + const [activeTab, setActiveTab] = useSyncQueryState( + 'activeTab', + StringParam, + 'all', + ); + + const [searchQuery, setSearchQuery] = useSyncQueryState( + 'search', + StringParam, + '', + ); + + return ( +
+ + + + + +

{t('Dashboards')}

+ + + setSearchQuery(e.currentTarget.value)} + /> + +
+
+ +
+
+ + + + +

{t('Recently Viewed')}

+ +
+
+ +
+
+ + + + +

{t('Favorites')}

+ +
+
+ +
+
+
+
+ ); } Welcome.propTypes = propTypes; diff --git a/superset/assets/tsconfig.json b/superset/assets/tsconfig.json index 44ed99ec2..6a505fa17 100644 --- a/superset/assets/tsconfig.json +++ b/superset/assets/tsconfig.json @@ -4,7 +4,7 @@ "outDir": "./dist", "module": "commonjs", "target": "es5", - "lib": ["es6", "dom"], + "lib": ["es6", "dom", "es2018.promise"], "sourceMap": true, "allowJs": true, "jsx": "react", @@ -17,7 +17,8 @@ "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, "noUnusedLocals": true, - "skipLibCheck": true + "skipLibCheck": true, + "esModuleInterop": true }, "include": ["./src/**/*", "./spec/**/*"] } diff --git a/superset/assets/tslint.json b/superset/assets/tslint.json index 2a53b5a37..021699e81 100644 --- a/superset/assets/tslint.json +++ b/superset/assets/tslint.json @@ -1,10 +1,11 @@ { - "extends": ["tslint:recommended", "tslint-react"], - "jsRules": { - }, - "rules": { - "interface-name" : [true, "never-prefix"], - "quotemark": [true, "single"] - }, - "rulesDirectory": [] -} \ No newline at end of file + "extends": ["tslint:recommended", "tslint-react"], + "jsRules": {}, + "rules": { + "interface-name": [true, "never-prefix"], + "quotemark": [true, "single"], + "jsx-no-multiline-js": false, + "jsx-no-lambda": false + }, + "rulesDirectory": [] +} diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index 74094d6ee..88f215daa 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -191,13 +191,6 @@ class Dashboard( # pylint: disable=too-many-instance-attributes return "" return f"/superset/profile/{self.changed_by.username}" - @property - def owners_json(self) -> List[Dict[str, Any]]: - owners = [] - for owner in self.owners: - owners.append({"name": owner.name}) - return owners - @property def data(self) -> Dict[str, Any]: positions = self.position_json diff --git a/superset/views/dashboard/api.py b/superset/views/dashboard/api.py index 92f74224b..4fcf938bb 100644 --- a/superset/views/dashboard/api.py +++ b/superset/views/dashboard/api.py @@ -180,7 +180,6 @@ class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi): "info": "list", "related": "list", } - exclude_route_methods = ("info",) show_columns = [ "dashboard_title", "slug", @@ -199,7 +198,6 @@ class DashboardRestApi(DashboardMixin, BaseSupersetModelRestApi): "dashboard_title", "url", "published", - "owners_json", "changed_by.username", "changed_by_name", "changed_by_url", diff --git a/superset/views/dashboard/views.py b/superset/views/dashboard/views.py index bb072df98..236f426a4 100644 --- a/superset/views/dashboard/views.py +++ b/superset/views/dashboard/views.py @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +import json import re from flask import g, redirect, request, Response @@ -30,10 +31,12 @@ from superset.utils import core as utils from ..base import ( BaseSupersetView, check_ownership, + common_bootstrap_payload, DeleteMixin, generate_download_headers, SupersetModelView, ) +from ..utils import bootstrap_user_data from .mixin import DashboardMixin @@ -43,6 +46,21 @@ class DashboardModelView( route_base = "/dashboard" datamodel = SQLAInterface(models.Dashboard) + @has_access + @expose("/list/") + def list(self): + payload = { + "user": bootstrap_user_data(g.user), + "common": common_bootstrap_payload(), + } + return self.render_template( + "superset/welcome.html", + entry="welcome", + bootstrap_data=json.dumps( + payload, default=utils.pessimistic_json_iso_dttm_ser + ), + ) + @action("mulexport", __("Export"), __("Export dashboards?"), "fa-database") def mulexport(self, items): # pylint: disable=no-self-use if not isinstance(items, list): diff --git a/superset/views/utils.py b/superset/views/utils.py index d69a871ed..9bcfc6eff 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -36,6 +36,8 @@ if not app.config["ENABLE_JAVASCRIPT_CONTROLS"]: def bootstrap_user_data(user, include_perms=False): + if user.is_anonymous: + return {} payload = { "username": user.username, "firstName": user.first_name, diff --git a/tests/dashboard_tests.py b/tests/dashboard_tests.py index 04e77066d..9ee6a3210 100644 --- a/tests/dashboard_tests.py +++ b/tests/dashboard_tests.py @@ -308,7 +308,7 @@ class DashboardTests(SupersetTestCase): resp = self.get_resp("/chart/list/") self.assertNotIn("birth_names", resp) - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn("/superset/dashboard/births/", resp) self.grant_public_access_to_table(table) @@ -316,7 +316,7 @@ class DashboardTests(SupersetTestCase): # Try access after adding appropriate permissions. self.assertIn("birth_names", self.get_resp("/chart/list/")) - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertIn("/superset/dashboard/births/", resp) self.assertIn("Births", self.get_resp("/superset/dashboard/births/")) @@ -325,7 +325,7 @@ class DashboardTests(SupersetTestCase): resp = self.get_resp("/chart/list/") self.assertNotIn("wb_health_population", resp) - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn("/superset/dashboard/world_health/", resp) def test_dashboard_with_created_by_can_be_accessed_by_public_users(self): @@ -374,7 +374,7 @@ class DashboardTests(SupersetTestCase): gamma_user = security_manager.find_user("gamma") self.login(gamma_user.username) - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn("/superset/dashboard/empty_dashboard/", resp) def test_users_can_view_published_dashboard(self): @@ -404,7 +404,7 @@ class DashboardTests(SupersetTestCase): db.session.merge(hidden_dash) db.session.commit() - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn(f"/superset/dashboard/{hidden_dash_slug}/", resp) self.assertIn(f"/superset/dashboard/{published_dash_slug}/", resp) @@ -432,7 +432,7 @@ class DashboardTests(SupersetTestCase): self.login(user.username) - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertIn(f"/superset/dashboard/{my_dash_slug}/", resp) self.assertNotIn(f"/superset/dashboard/{not_my_dash_slug}/", resp) @@ -465,7 +465,7 @@ class DashboardTests(SupersetTestCase): self.login(user.username) - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertIn(f"/superset/dashboard/{fav_dash_slug}/", resp) def test_user_can_not_view_unpublished_dash(self): @@ -485,7 +485,7 @@ class DashboardTests(SupersetTestCase): # list dashboards as a gamma user self.login(gamma_user.username) - resp = self.get_resp("/dashboard/list/") + resp = self.get_resp("/api/v1/dashboard/") self.assertNotIn(f"/superset/dashboard/{slug}/", resp) diff --git a/tests/security_tests.py b/tests/security_tests.py index 67877bc5a..afaad3866 100644 --- a/tests/security_tests.py +++ b/tests/security_tests.py @@ -458,7 +458,7 @@ class RolePermissionTests(SupersetTestCase): def test_gamma_user_schema_access_to_dashboards(self): self.login(username="gamma") - data = str(self.client.get("dashboard/list/").data) + data = str(self.client.get("api/v1/dashboard/").data) self.assertIn("/superset/dashboard/world_health/", data) self.assertNotIn("/superset/dashboard/births/", data)