You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2020/01/16 05:49:07 UTC

[incubator-superset] branch master updated: [dashboard] New, list view (react) (#8845)

This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b97764  [dashboard] New, list view (react) (#8845)
7b97764 is described below

commit 7b97764dbc192740fc64ada59cdeb809232d3676
Author: ʈᵃᵢ <td...@gmail.com>
AuthorDate: Wed Jan 15 21:48:55 2020 -0800

    [dashboard] New, list view (react) (#8845)
    
    * adds dashboard listview component
    
    * use new api
    
    * use json over rison
    
    * lint
    
    * adds seperate dashboard list view
    
    * edit and delete actions
    
    * fix lint ignore
    
    * fix common_bootstrap_payload is now a function
    
    * fix license
    
    * fix pylint
    
    * isort
    
    * fix tests
    
    * lint
    
    * lint ts
    
    * fix js tests
    
    * fix double import from bad rebase
    
    * fix indent error
    
    * lookup permissions
    
    * generic permission lookup
    
    * get tslint to pass
    
    * adds js specs
    
    * lint
    
    * fix rebase
    
    * lint
    
    * lint again
    
    * fix type errors preventing build
    
    * adds more specs
    
    * fix tslint error
    
    * fix null check
    
    * remove unecessary code
    
    * use translations provided by api
    
    * more translations
    
    * linting
    
    * fix spec
    
    * i18n
    
    * fix register order
---
 superset/assets/.babelrc                           |  15 +-
 superset/assets/jest.config.js                     |   7 +
 superset/assets/package-lock.json                  | 577 +++++++++------------
 superset/assets/package.json                       |  12 +-
 .../components/ListView/ListView_spec.jsx          | 175 +++++++
 .../explore/components/ExploreChartHeader_spec.jsx |   4 +-
 .../views/dashboardList/DashboardList_spec.jsx     |  82 +++
 .../javascripts/welcome/DashboardTable_spec.jsx    |  10 +-
 .../assets/src/components/ListView/ListView.tsx    | 254 +++++++++
 .../components/ListView/ListViewStyles.less}       |  53 +-
 .../src/components/ListView/TableCollection.tsx    |  90 ++++
 .../{.babelrc => src/components/ListView/types.ts} |  49 +-
 superset/assets/src/components/ListView/utils.ts   | 172 ++++++
 superset/assets/src/types/react-table.d.ts         | 243 +++++++++
 .../views/dashboardList/DashboardList.less}        |  18 +-
 .../src/views/dashboardList/DashboardList.tsx      | 294 +++++++++++
 superset/assets/src/welcome/App.jsx                |  11 +-
 superset/assets/src/welcome/DashboardTable.jsx     | 166 +++---
 superset/assets/src/welcome/Welcome.jsx            | 143 ++---
 superset/assets/tsconfig.json                      |   5 +-
 superset/assets/tslint.json                        |  19 +-
 superset/models/dashboard.py                       |   7 -
 superset/views/dashboard/api.py                    |   2 -
 superset/views/dashboard/views.py                  |  18 +
 superset/views/utils.py                            |   2 +
 tests/dashboard_tests.py                           |  16 +-
 tests/security_tests.py                            |   2 +-
 27 files changed, 1919 insertions(+), 527 deletions(-)

diff --git a/superset/assets/.babelrc b/superset/assets/.babelrc
index 761abe9..acf603c 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 5c31324..19b984b 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 66fcd3e..832355b 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 2add582..0312de2 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 0000000..79ace2c
--- /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(<ListView {...mockedProps} />);
+
+  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 ec7278f..72e03c5 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 0000000..51026e6
--- /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(<DashboardList {...mockedProps} />, {
+    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 42f2dd7..a519695 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 0000000..f236d74
--- /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<Props> = ({
+  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 <Loading />;
+  }
+
+  return (
+    <div className={`superset-list-view ${className}`}>
+      {title && filterable && (
+        <div className='header'>
+          <Row>
+            <Col md={10}>
+              <h2>{t(title)}</h2>
+            </Col>
+            {filterable && (
+              <Col md={2}>
+                <div className='filter-dropdown'>
+                  <DropdownButton
+                    bsSize='small'
+                    bsStyle={'default'}
+                    noCaret={true}
+                    title={(
+                      <>
+                        <i className='fa fa-filter text-primary' />
+                        {'  '}{t('Filter List')}
+                      </>
+                    )}
+                    id={'filter-picker'}
+                  >
+                    {filterableColumns
+                      .map(({ id, accessor, Header }) => ({
+                        Header,
+                        id: id || accessor,
+                      }))
+                      .map((ft: FilterToggle) => (
+                        <MenuItem
+                          key={ft.id}
+                          eventKey={ft}
+                          onSelect={(fltr: FilterToggle) => {
+                            setFilterToggles([...filterToggles, fltr]);
+                          }
+                          }
+                        >
+                          {ft.Header}
+                        </MenuItem>
+                      ))}
+                  </DropdownButton>
+                </div>
+              </Col>
+            )}
+          </Row>
+          <hr />
+          {filterToggles.map((ft, i) => (
+            <div key={`${ft.Header}-${i}`} className='filter-inputs'>
+              <Row>
+                <Col className='text-center filter-column' md={2}>
+                  <span>{ft.Header}</span>
+                </Col>
+                <Col md={2}>
+                  <FormControl
+                    componentClass='select'
+                    bsSize='small'
+                    value={ft.filterId}
+                    placeholder={filterTypes[ft.id][0].name}
+                    onChange={(e: React.MouseEvent<HTMLInputElement>) =>
+                      updateFilterToggle(i, { filterId: e.currentTarget.value })
+                    }
+                  >
+                    {filterTypes[ft.id] && filterTypes[ft.id].map(
+                      ({ name, operator }: FilterType) => (
+                        <option key={name} value={operator}>
+                          {name}
+                        </option>
+                      ),
+                    )}
+                  </FormControl>
+                </Col>
+                <Col md={1} />
+                <Col md={4}>
+                  <FormControl
+                    type='text'
+                    bsSize='small'
+                    value={ft.filterValue || ''}
+                    onChange={(e: React.KeyboardEvent<HTMLInputElement>) =>
+                      updateFilterToggle(i, {
+                        filterValue: e.currentTarget.value,
+                      })
+                    }
+                  />
+                </Col>
+                <Col md={1}>
+                  <div
+                    className='filter-close'
+                    role='button'
+                    onClick={() => removeFilterAndApply(i)}
+                  >
+                    <i className='fa fa-close text-primary' />
+                  </div>
+                </Col>
+              </Row>
+              <br />
+            </div>
+          ))}
+          {filterToggles.length > 0 && (
+            <>
+              <Row>
+                <Col md={10} />
+                <Col md={2}>
+                  <Button
+                    data-test='apply-filters'
+                    disabled={filtersApplied ? true : false}
+                    bsStyle='primary'
+                    onClick={applyFilters}
+                    bsSize='small'
+                  >
+                    {t('Apply')}
+                  </Button>
+                </Col>
+              </Row>
+              <br />
+            </>
+          )}
+        </div>
+      )
+      }
+      <div className='body'>
+        <TableCollection
+          getTableProps={getTableProps}
+          getTableBodyProps={getTableBodyProps}
+          prepareRow={prepareRow}
+          headerGroups={headerGroups}
+          rows={rows}
+          loading={loading}
+        />
+      </div>
+      <div className='footer'>
+        <Pagination
+          prev={canPreviousPage}
+          first={pageIndex > 1}
+          next={canNextPage}
+          last={pageIndex < pageCount - 2}
+          items={pageCount}
+          activePage={pageIndex + 1}
+          ellipsis={true}
+          boundaryLinks={true}
+          maxButtons={5}
+          onSelect={(p: number) => gotoPage(p - 1)}
+        />
+        <span className='pull-right'>
+          {t('showing')}{' '}
+          <strong>
+            {pageSize * pageIndex + (rows.length && 1)}-
+            {pageSize * pageIndex + rows.length}
+          </strong>{' '}
+          {t('of')} <strong>{count}</strong>
+        </span>
+      </div>
+    </div >
+  );
+};
+
+export default ListView;
diff --git a/superset/assets/.babelrc b/superset/assets/src/components/ListView/ListViewStyles.less
similarity index 56%
copy from superset/assets/.babelrc
copy to superset/assets/src/components/ListView/ListViewStyles.less
index 761abe9..e163a4b 100644
--- a/superset/assets/.babelrc
+++ b/superset/assets/src/components/ListView/ListViewStyles.less
@@ -16,16 +16,49 @@
  * 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"],
-  "env": {
-    "test": {
-      "plugins": [
-        "babel-plugin-dynamic-import-node"
-      ]
+.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 0000000..f914b2b
--- /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<D> {
+  getTableProps: (userProps?: any) => any;
+  getTableBodyProps: (userProps?: any) => any;
+  prepareRow: (row: Row<D>) => any;
+  headerGroups: Array<HeaderGroup<D>>;
+  rows: Array<Row<D>>;
+  loading: boolean;
+}
+/* tslint:disable:jsx-key */
+export default function TableCollection({
+  getTableProps,
+  getTableBodyProps,
+  prepareRow,
+  headerGroups,
+  rows,
+  loading,
+}: Props<any>) {
+  return (
+    <table {...getTableProps()} className='table table-hover'>
+      <thead>
+        {headerGroups.map((headerGroup) => (
+          <tr {...headerGroup.getHeaderGroupProps()}>
+            {headerGroup.headers.map((column: any) => (
+              <th {...column.getHeaderProps(column.getSortByToggleProps())} data-test='sort-header'>
+                {column.render('Header')}
+                {'  '}
+                {column.sortable && (
+                  <i
+                    className={`text-primary fa fa-${
+                      column.isSorted
+                        ? column.isSortedDesc
+                          ? 'sort-down'
+                          : 'sort-up'
+                        : 'sort'
+                      }`}
+                  />
+                )}
+              </th>
+            ))}
+          </tr>
+        ))}
+      </thead>
+      <tbody {...getTableBodyProps()}>
+        {rows.map((row) => {
+          prepareRow(row);
+          const loadingProps = loading ? { className: 'table-row-loader' } : {};
+          return (
+            <tr
+              {...row.getRowProps()}
+              {...loadingProps}
+              onMouseEnter={() => row.setState && row.setState({ hover: true })}
+              onMouseLeave={() => row.setState && row.setState({ hover: false })}
+            >
+              {row.cells.map((cell: Cell<any>) => {
+                const columnCellProps = cell.column.cellProps || {};
+
+                return (
+                  <td {...cell.getCellProps()} {...columnCellProps}>
+                    <span>{cell.render('Cell')}</span>
+                  </td>
+                );
+              })}
+            </tr>
+          );
+        })}
+      </tbody>
+    </table>
+  );
+}
diff --git a/superset/assets/.babelrc b/superset/assets/src/components/ListView/types.ts
similarity index 57%
copy from superset/assets/.babelrc
copy to superset/assets/src/components/ListView/types.ts
index 761abe9..0151d45 100644
--- a/superset/assets/.babelrc
+++ b/superset/assets/src/components/ListView/types.ts
@@ -16,16 +16,41 @@
  * 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"],
-  "env": {
-    "test": {
-      "plugins": [
-        "babel-plugin-dynamic-import-node"
-      ]
-    }
-  }
+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 0000000..d62de94
--- /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<FilterToggle[]>(
+    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 0000000..5a1e727
--- /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 <https://github.com/grsmto>
+//                 Artem Berdyshev <https://github.com/berdyshev>
+//                 Christian Murphy <https://github.com/ChristianMurphy>
+//                 Tai Dupreee <https://github.com/nytai>
+// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
+// TypeScript Version: 3.0
+declare module 'react-table' {
+  import { Dispatch, ReactNode, SetStateAction } from 'react';
+
+  export interface Cell<D> {
+    render: (type: string) => any;
+    getCellProps: () => { key: string; [k: string]: any };
+    column: Column<D>;
+    row: Row<D>;
+    state: any;
+    value: any;
+  }
+
+  export interface Row<D> {
+    index: number;
+    cells: Array<Cell<D>>;
+    getRowProps: () => { key: string; [k: string]: any };
+    original: any;
+    state?: any;
+    setState?: (state: any) => any;
+  }
+
+  export interface HeaderColumn<D, A extends keyof D = never> {
+    /**
+     * This string/function is used to build the data model for your column.
+     */
+    accessor: A | ((originalRow: D) => string);
+    Header?: string | ((props: TableInstance<D>) => ReactNode);
+    Filter?: string | ((props: TableInstance<D>) => ReactNode);
+    Cell?: string | ((cell: Cell<D>) => 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<D, A extends keyof D = never>
+    extends HeaderColumn<D, A> {
+    id: string | number;
+  }
+
+  export type Page<D> = Array<Row<D>>;
+
+  export interface EnhancedColumn<D, A extends keyof D = never>
+    extends Column<D, A> {
+    render: (type: string) => any;
+    getHeaderProps: (userProps?: any) => any;
+    getSortByToggleProps: (userProps?: any) => any;
+    sorted: boolean;
+    sortedDesc: boolean;
+    sortedIndex: number;
+  }
+
+  export interface HeaderGroup<D, A extends keyof D = never> {
+    headers: Array<EnhancedColumn<D, A>>;
+    getRowProps: (userProps?: any) => any;
+    getHeaderGroupProps: (userProps?: any) => any;
+  }
+
+  export interface Hooks<D> {
+    beforeRender: [];
+    columns: [];
+    headerGroups: [];
+    headers: [];
+    rows: Array<Row<D>>;
+    row: [];
+    renderableRows: [];
+    getTableProps: [];
+    getRowProps: [];
+    getHeaderRowProps: [];
+    getHeaderProps: [];
+    getCellProps: [];
+  }
+
+  export interface TableInstance<D>
+    extends TableOptions<D>,
+      UseRowsValues<D>,
+      UseFiltersValues,
+      UsePaginationValues<D>,
+      UseColumnsValues<D>,
+      UseRowStateValues<D> {
+    hooks: Hooks<D>;
+    rows: Array<Row<D>>;
+    columns: Array<EnhancedColumn<D>>;
+    getTableProps: (userProps?: any) => any;
+    getTableBodyProps: (userProps?: any) => any;
+    getRowProps: (userProps?: any) => any;
+    prepareRow: (row: Row<D>) => any;
+    getSelectRowToggleProps: (userProps?: any) => any;
+    toggleSelectAll: (forcedState: boolean) => any;
+    state: { [key: string]: any };
+  }
+
+  export interface TableOptions<D> {
+    data: D[];
+    columns: Array<HeaderColumn<D>>;
+    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<D> {
+    nextPage: () => any;
+    previousPage: () => any;
+    setPageSize: (size: number) => any;
+    gotoPage: (page: number) => any;
+    canPreviousPage: boolean;
+    canNextPage: boolean;
+    page: Page<D>;
+    pageOptions: [];
+  }
+
+  export interface UseRowsValues<D> {
+    rows: Array<Row<D>>;
+  }
+
+  export interface UseColumnsValues<D> {
+    columns: Array<EnhancedColumn<D>>;
+    headerGroups: Array<HeaderGroup<D>>;
+    headers: Array<EnhancedColumn<D>>;
+  }
+
+  export interface UseFiltersValues {
+    setFilter: (columnId: string, filter: string) => any;
+    setAllFilters: (filterObj: any) => any;
+  }
+
+  export interface UseRowStateValues<D> {
+    setRowState: (rowPath: string[], updater: (state: any) => any) => any;
+  }
+
+  export function useTable<D>(
+    props: TableOptions<D>,
+    ...plugins: any[]
+  ): TableInstance<D>;
+
+  export function useColumns<D>(
+    props: TableOptions<D>,
+  ): TableOptions<D> & UseColumnsValues<D>;
+
+  export function useRows<D>(
+    props: TableOptions<D>,
+  ): TableOptions<D> & UseRowsValues<D>;
+
+  export function useFilters<D>(
+    props: TableOptions<D>,
+  ): TableOptions<D> & {
+    rows: Array<Row<D>>;
+  };
+
+  export function useSortBy<D>(
+    props: TableOptions<D>,
+  ): TableOptions<D> & {
+    rows: Array<Row<D>>;
+  };
+
+  export function useGroupBy<D>(
+    props: TableOptions<D>,
+  ): TableOptions<D> & { rows: Array<Row<D>> };
+
+  export function usePagination<D>(
+    props: TableOptions<D>,
+  ): UsePaginationValues<D>;
+
+  export function useRowState<D>(props: TableOptions<D>): UseRowStateValues<D>;
+
+  export function useFlexLayout<D>(props: TableOptions<D>): TableOptions<D>;
+
+  export function useExpanded<D>(
+    props: TableOptions<D>,
+  ): TableOptions<D> & {
+    toggleExpandedByPath: () => any;
+    expandedDepth: [];
+    rows: [];
+  };
+
+  export function useTableState(
+    initialState?: any,
+    overriddenState?: any,
+    options?: {
+      reducer?: (oldState: any, newState: any, type: string) => any;
+      useState?: [any, Dispatch<SetStateAction<any>>];
+    },
+  ): any;
+
+  export const actions: any;
+}
diff --git a/superset/assets/.babelrc b/superset/assets/src/views/dashboardList/DashboardList.less
similarity index 72%
copy from superset/assets/.babelrc
copy to superset/assets/src/views/dashboardList/DashboardList.less
index 761abe9..fd58fb8 100644
--- a/superset/assets/.babelrc
+++ b/superset/assets/src/views/dashboardList/DashboardList.less
@@ -16,16 +16,12 @@
  * 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"],
-  "env": {
-    "test": {
-      "plugins": [
-        "babel-plugin-dynamic-import-node"
-      ]
-    }
+.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 0000000..0bc2823
--- /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<Props, State> {
+
+  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) => <a href={url}>{dashboard_title}</a>,
+        Header: this.state.labelColumns.dashboard_title || '',
+        accessor: 'dashboard_title',
+        filterable: true,
+        sortable: true,
+      },
+      {
+        Cell: ({
+          row: {
+            original: { changed_by_name, changed_by_url },
+          },
+        }: any) => <a href={changed_by_url}>{changed_by_name}</a>,
+        Header: this.state.labelColumns.changed_by_name || '',
+        accessor: 'changed_by_fk',
+        sortable: true,
+      },
+      {
+        Cell: ({
+          row: {
+            original: { published },
+          },
+        }: any) => (
+            <span className='no-wrap'>{published ? <i className='fa fa-check' /> : ''}</span>
+          ),
+        Header: this.state.labelColumns.published || '',
+        accessor: 'published',
+        sortable: true,
+      },
+      {
+        Cell: ({
+          row: {
+            original: { changed_on },
+          },
+        }: any) => (
+            <span className='no-wrap'>{moment(changed_on).fromNow()}</span>
+          ),
+        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 (
+            <span className={`actions ${state && state.hover ? '' : 'invisible'}`}>
+              {this.canDelete && (
+                <span
+                  role='button'
+                  className='action-button'
+                  onClick={handleDelete}
+                >
+                  <i className='fa fa-trash' />
+                </span>
+              )}
+              {this.canEdit && (
+                <span
+                  role='button'
+                  className='action-button'
+                  onClick={handleEdit}
+                >
+                  <i className='fa fa-pencil' />
+                </span>
+              )}
+            </span>
+          );
+        },
+        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 (
+      <div className='container welcome'>
+        <Panel>
+          <ListView
+            className='dashboard-list-view'
+            title={'Dashboards'}
+            columns={this.columns}
+            data={dashboards}
+            count={dashboardCount}
+            pageSize={PAGE_SIZE}
+            fetchData={this.fetchData}
+            loading={loading}
+            initialSort={this.initialSort}
+            filterTypes={filterTypes}
+          />
+        </Panel>
+
+        <Modal show={this.state.showDeleteModal} onHide={this.toggleModal}>
+          <Modal.Header closeButton={true} />
+          <Modal.Body>
+            {t('Are you sure you want to delete')}{' '}
+            <b>{this.state.deleteCandidate.dashboard_title}</b>?
+          </Modal.Body>
+          <Modal.Footer>
+            <Button onClick={this.toggleModal}>{t('Cancel')}</Button>
+            <Button bsStyle='danger' onClick={this.handleDashboardDelete}>
+              {t('OK')}
+            </Button>
+          </Modal.Footer>
+        </Modal>
+      </div>
+    );
+  }
+}
+
+export default withToasts(DashboardList);
diff --git a/superset/assets/src/welcome/App.jsx b/superset/assets/src/welcome/App.jsx
index 5e12bc1..77fe6aa 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 = () => (
     <Router>
       <Menu data={menu} />
       <Switch>
-        <Route path="/superset/welcome">
+        <Route path="/superset/welcome/">
           <Welcome user={user} />
-          <ToastPresenter />
+        </Route>
+        <Route path="/dashboard/list/">
+          <DashboardList user={user} />
         </Route>
       </Switch>
+      <ToastPresenter />
     </Router>
   </Provider>
 );
diff --git a/superset/assets/src/welcome/DashboardTable.jsx b/superset/assets/src/welcome/DashboardTable.jsx
index 079fd0e..c8fbea2 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 },
+        },
+      }) => <a href={url}>{dashboardTitle}</a>,
+    },
+    {
+      accessor: 'changed_by_fk',
+      Header: 'Creator',
+      sortable: true,
+      Cell: ({
+        row: {
+          original: { changed_by_name: changedByName, changedByUrl },
+        },
+      }) => <a href={changedByUrl}>{changedByName}</a>,
+    },
+    {
+      accessor: 'changed_on',
+      Header: 'Modified',
+      sortable: true,
+      Cell: ({
+        row: {
+          original: { changed_on: changedOn },
+        },
+      }) => <span className="no-wrap">{moment(changedOn).fromNow()}</span>,
+    },
+  ];
+
+  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 }));
+  };
 
-  render() {
-    if (this.state.dashboards !== null) {
-      return (
-        <Table
-          className="table"
-          sortable={['dashboard', 'creator', 'modified']}
-          filterBy={this.props.search}
-          filterable={['dashboard', 'creator']}
-          itemsPerPage={50}
-          hideFilterInput
-          columns={[
-            { key: 'dashboard', label: 'Dashboard' },
-            { key: 'creator', label: 'Creator' },
-            { key: 'modified', label: 'Modified' },
-          ]}
-          defaultSort={{ column: 'modified', direction: 'desc' }}
-        >
-          {this.state.dashboards.map(o => (
-            <Tr key={o.id}>
-              <Td column="dashboard" value={o.dashboard_title}>
-                <a href={o.url}>{o.dashboard_title}</a>
-              </Td>
-              <Td column="creator" value={o.changed_by_name}>
-                {unsafe(o.creator)}
-              </Td>
-              <Td column="modified" value={o.changed_on} className="text-muted">
-                {unsafe(o.modified)}
-              </Td>
-            </Tr>
-          ))}
-        </Table>
-      );
-    }
+  fetchDataDebounced = debounce(this.fetchData, 200);
 
-    return <Loading />;
+  render() {
+    return (
+      <ListView
+        columns={this.columns}
+        data={this.state.dashboards}
+        count={this.state.dashboard_count}
+        pageSize={PAGE_SIZE}
+        fetchData={this.fetchData}
+        loading={this.state.loading}
+        initialSort={this.initialSort}
+      />
+    );
   }
 }
 
-DashboardTable.propTypes = propTypes;
-
 export default withToasts(DashboardTable);
diff --git a/superset/assets/src/welcome/Welcome.jsx b/superset/assets/src/welcome/Welcome.jsx
index 54e9862..3b7a2f1 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 (
-      <div className="container welcome">
-        <Tabs defaultActiveKey={1} id="uncontrolled-tab-example">
-          <Tab eventKey={1} title={t('Dashboards')}>
-            <Panel>
-              <Row>
-                <Col md={8}>
-                  <h2>{t('Dashboards')}</h2>
-                </Col>
-                <Col md={4}>
-                  <FormControl
-                    type="text"
-                    bsSize="sm"
-                    style={{ marginTop: '25px' }}
-                    placeholder="Search"
-                    value={this.state.search}
-                    onChange={this.onSearchChange}
-                  />
-                </Col>
-              </Row>
-              <hr />
-              <DashboardTable search={this.state.search} />
-            </Panel>
-          </Tab>
-          <Tab eventKey={2} title={t('Recently Viewed')}>
-            <Panel>
-              <Row>
-                <Col md={8}>
-                  <h2>{t('Recently Viewed')}</h2>
-                </Col>
-              </Row>
-              <hr />
-              <RecentActivity user={this.props.user} />
-            </Panel>
-          </Tab>
-          <Tab eventKey={3} title={t('Favorites')}>
-            <Panel>
-              <Row>
-                <Col md={8}>
-                  <h2>{t('Favorites')}</h2>
-                </Col>
-              </Row>
-              <hr />
-              <Favorites user={this.props.user} />
-            </Panel>
-          </Tab>
-        </Tabs>
-      </div>
-    );
-  }
+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 (
+    <div className="container welcome">
+      <Tabs
+        activeKey={activeTab}
+        onSelect={setActiveTab}
+        id="uncontrolled-tab-example"
+      >
+        <Tab eventKey="all" title={t('Dashboards')}>
+          <Panel>
+            <Row>
+              <Col md={8}>
+                <h2>{t('Dashboards')}</h2>
+              </Col>
+              <Col md={4}>
+                <FormControl
+                  type="text"
+                  bsSize="sm"
+                  style={{ marginTop: '25px' }}
+                  placeholder="Search"
+                  value={searchQuery}
+                  onChange={e => setSearchQuery(e.currentTarget.value)}
+                />
+              </Col>
+            </Row>
+            <hr />
+            <DashboardTable search={searchQuery} />
+          </Panel>
+        </Tab>
+        <Tab eventKey="recent" title={t('Recently Viewed')}>
+          <Panel>
+            <Row>
+              <Col md={8}>
+                <h2>{t('Recently Viewed')}</h2>
+              </Col>
+            </Row>
+            <hr />
+            <RecentActivity user={user} />
+          </Panel>
+        </Tab>
+        <Tab eventKey="favorites" title={t('Favorites')}>
+          <Panel>
+            <Row>
+              <Col md={8}>
+                <h2>{t('Favorites')}</h2>
+              </Col>
+            </Row>
+            <hr />
+            <Favorites user={user} />
+          </Panel>
+        </Tab>
+      </Tabs>
+    </div>
+  );
 }
 
 Welcome.propTypes = propTypes;
diff --git a/superset/assets/tsconfig.json b/superset/assets/tsconfig.json
index 44ed99e..6a505fa 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 2a53b5a..021699e 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 74094d6..88f215d 100644
--- a/superset/models/dashboard.py
+++ b/superset/models/dashboard.py
@@ -192,13 +192,6 @@ class Dashboard(  # pylint: disable=too-many-instance-attributes
         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
         if positions:
diff --git a/superset/views/dashboard/api.py b/superset/views/dashboard/api.py
index 92f7422..4fcf938 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 bb072df..236f426 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 d69a871..9bcfc6e 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 04e7706..9ee6a32 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</a>", 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</a>", 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 67877bc..afaad38 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)