You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2020/10/30 05:00:12 UTC

[incubator-superset] branch master updated: feat: home screen mvp (#11206)

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

rusackas 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 f7051ea  feat: home screen mvp  (#11206)
f7051ea is described below

commit f7051eaaded4e371f8a04f82915969bbaeb3763e
Author: Phillip Kelley-Dotson <pk...@yahoo.com>
AuthorDate: Thu Oct 29 21:59:31 2020 -0700

    feat: home screen mvp  (#11206)
    
    * step 1: broken stuff!
    
    * first steps
    
    * more adding and slicing
    
    * step 1: broken stuff!
    
    * can now filter dashboards/charts for "Edited" tabs (filter by changed_by o_m)
    
    * more updates
    
    * update recent card
    
    * add icon
    
    * Adding Expand Icon to Collapse component
    
    * more updates
    
    * clean up code
    
    * remove lock file
    
    * remove consoles
    
    * fixing subnav button height shift
    
    * lil' ascii arrows
    
    * update branch
    
    * update test part 1
    
    * remove consoles
    
    * fix typescript
    
    * add images and update emptystate
    
    * add changes
    
    * update chart card
    
    * fix css issues from rebase
    
    * add suggestions
    
    * more changes
    
    * update tests and clear typescript errors
    
    * Update superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * update from comments
    
    * more updates..
    
    * fix rebase
    
    * fix pesky type errors
    
    * test fixes
    
    * lint fix
    
    * Update superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/components/Menu/SubMenu.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/components/ListViewCard/index.tsx
    
    Co-authored-by: ʈᵃᵢ <td...@gmail.com>
    
    * Update superset-frontend/src/components/ListViewCard/index.tsx
    
    Co-authored-by: ʈᵃᵢ <td...@gmail.com>
    
    * add suggestions
    
    * fix lint
    
    * remove unused code
    
    * toast getrecentActivityobjs
    
    * add some suggestions
    
    * remove types for now
    
    * cypress fix
    
    * remove unused type
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    Co-authored-by: ʈᵃᵢ <td...@gmail.com>
---
 superset-frontend/images/empty-charts.png          | Bin 0 -> 2274 bytes
 superset-frontend/images/empty-dashboard.png       | Bin 0 -> 1467 bytes
 superset-frontend/images/empty-queries.png         | Bin 0 -> 1791 bytes
 superset-frontend/images/star-circle.png           | Bin 0 -> 2705 bytes
 superset-frontend/images/union.png                 | Bin 0 -> 1694 bytes
 .../spec/javascripts/components/SubMenu_spec.jsx   |   2 +-
 .../views/CRUD/chart/ChartList_spec.jsx            |   1 +
 .../views/CRUD/welcome/ActivityTable_spec.tsx      |  87 ++++++
 .../views/CRUD/welcome/ChartTable_spec.tsx         |  79 ++++++
 .../views/CRUD/welcome/DashboardTable_spec.tsx     |  78 ++++--
 .../views/CRUD/welcome/EmptyState_spec.tsx         |  92 +++++++
 .../views/CRUD/welcome/SavedQueries_spec.tsx       | 106 ++++++++
 .../views/CRUD/welcome/Welcome_spec.tsx            |  23 +-
 .../src/components/ListViewCard/index.tsx          |  60 +++--
 superset-frontend/src/components/Menu/SubMenu.tsx  |  45 ++--
 .../src/views/CRUD/chart/ChartCard.tsx             | 138 ++++++++++
 .../src/views/CRUD/chart/ChartList.tsx             | 144 +++-------
 .../src/views/CRUD/dashboard/DashboardCard.tsx     | 140 ++++++++++
 .../src/views/CRUD/dashboard/DashboardList.tsx     | 138 ++--------
 superset-frontend/src/views/CRUD/data/common.ts    |   2 +-
 .../views/CRUD/data/savedquery/SavedQueryList.tsx  |  15 +-
 superset-frontend/src/views/CRUD/hooks.ts          |  85 +++++-
 superset-frontend/src/views/CRUD/types.ts          |  49 ++++
 superset-frontend/src/views/CRUD/utils.tsx         | 197 ++++++++++++++
 .../src/views/CRUD/welcome/ActivityTable.tsx       | 209 ++++++++++++++
 .../src/views/CRUD/welcome/ChartTable.tsx          | 167 ++++++++++++
 .../src/views/CRUD/welcome/DashboardTable.tsx      | 299 +++++++++++----------
 .../src/views/CRUD/welcome/EmptyState.tsx          | 144 ++++++++++
 .../src/views/CRUD/welcome/SavedQueries.tsx        | 260 ++++++++++++++++++
 .../src/views/CRUD/welcome/Welcome.tsx             | 177 +++++-------
 superset/charts/api.py                             |   1 +
 superset/dashboards/api.py                         |   1 +
 superset/queries/api.py                            |   3 +
 superset/queries/saved_queries/api.py              |   2 +-
 34 files changed, 2177 insertions(+), 567 deletions(-)

diff --git a/superset-frontend/images/empty-charts.png b/superset-frontend/images/empty-charts.png
new file mode 100644
index 0000000..b814d35
Binary files /dev/null and b/superset-frontend/images/empty-charts.png differ
diff --git a/superset-frontend/images/empty-dashboard.png b/superset-frontend/images/empty-dashboard.png
new file mode 100644
index 0000000..b0d4462
Binary files /dev/null and b/superset-frontend/images/empty-dashboard.png differ
diff --git a/superset-frontend/images/empty-queries.png b/superset-frontend/images/empty-queries.png
new file mode 100644
index 0000000..adc51c0
Binary files /dev/null and b/superset-frontend/images/empty-queries.png differ
diff --git a/superset-frontend/images/star-circle.png b/superset-frontend/images/star-circle.png
new file mode 100644
index 0000000..77fd94d
Binary files /dev/null and b/superset-frontend/images/star-circle.png differ
diff --git a/superset-frontend/images/union.png b/superset-frontend/images/union.png
new file mode 100644
index 0000000..af94c07
Binary files /dev/null and b/superset-frontend/images/union.png differ
diff --git a/superset-frontend/spec/javascripts/components/SubMenu_spec.jsx b/superset-frontend/spec/javascripts/components/SubMenu_spec.jsx
index 54a1b63..ab0020d 100644
--- a/superset-frontend/spec/javascripts/components/SubMenu_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/SubMenu_spec.jsx
@@ -24,7 +24,7 @@ import SubMenu from 'src/components/Menu/SubMenu';
 
 const defaultProps = {
   name: 'Title',
-  children: [
+  tabs: [
     {
       name: 'Page1',
       label: 'Page1',
diff --git a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
index 3872358..3abedc3 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
@@ -54,6 +54,7 @@ const mockCharts = [...new Array(3)].map((_, i) => ({
 fetchMock.get(chartsInfoEndpoint, {
   permissions: ['can_list', 'can_edit', 'can_delete'],
 });
+
 fetchMock.get(chartssOwnersEndpoint, {
   result: [],
 });
diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx
new file mode 100644
index 0000000..eba5d66
--- /dev/null
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx
@@ -0,0 +1,87 @@
+/**
+ * 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 { styledMount as mount } from 'spec/helpers/theming';
+import thunk from 'redux-thunk';
+import fetchMock from 'fetch-mock';
+
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import configureStore from 'redux-mock-store';
+import ActivityTable from 'src/views/CRUD/welcome/ActivityTable';
+
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
+const chartsEndpoint = 'glob:*/api/v1/chart/?*';
+const dashboardEndpoint = 'glob:*/api/v1/dashboard/?*';
+const savedQueryEndpoint = 'glob:*/api/v1/saved_query/?*';
+
+fetchMock.get(chartsEndpoint, {
+  result: [
+    {
+      slice_name: 'ChartyChart',
+      changed_on_utc: '24 Feb 2014 10:13:14',
+      url: '/fakeUrl/explore',
+      id: '4',
+      table: {},
+    },
+  ],
+});
+
+fetchMock.get(dashboardEndpoint, {
+  result: [
+    {
+      dashboard_title: 'Dashboard_Test',
+      changed_on_utc: '24 Feb 2014 10:13:14',
+      url: '/fakeUrl/dashboard',
+      id: '3',
+    },
+  ],
+});
+
+fetchMock.get(savedQueryEndpoint, {
+  result: [],
+});
+
+describe('ActivityTable', () => {
+  const activityProps = {
+    user: {
+      userId: '1',
+    },
+    activityFilter: 'Edited',
+  };
+  const wrapper = mount(<ActivityTable {...activityProps} />, {
+    context: { store },
+  });
+
+  beforeAll(async () => {
+    await waitForComponentToPaint(wrapper);
+  });
+
+  it('the component renders ', () => {
+    expect(wrapper.find(ActivityTable)).toExist();
+  });
+
+  it('calls batch method and renders ListViewCArd', async () => {
+    const chartCall = fetchMock.calls(/chart\/\?q/);
+    const dashboardCall = fetchMock.calls(/dashboard\/\?q/);
+    expect(chartCall).toHaveLength(2);
+    expect(dashboardCall).toHaveLength(2);
+  });
+});
diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx
new file mode 100644
index 0000000..f8cd053
--- /dev/null
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx
@@ -0,0 +1,79 @@
+/**
+ * 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 { styledMount as mount } from 'spec/helpers/theming';
+import thunk from 'redux-thunk';
+import fetchMock from 'fetch-mock';
+import configureStore from 'redux-mock-store';
+
+import ChartTable from 'src/views/CRUD/welcome/ChartTable';
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
+const chartsEndpoint = 'glob:*/api/v1/chart/?*';
+const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*';
+
+const mockCharts = [...new Array(3)].map((_, i) => ({
+  changed_on_utc: new Date().toISOString(),
+  created_by: 'super user',
+  id: i,
+  slice_name: `cool chart ${i}`,
+  url: 'url',
+  viz_type: 'bar',
+  datasource_title: `ds${i}`,
+  thumbnail_url: '',
+}));
+
+fetchMock.get(chartsEndpoint, {
+  result: mockCharts,
+});
+
+fetchMock.get(chartsInfoEndpoint, {
+  permissions: ['can_add', 'can_edit', 'can_delete'],
+});
+
+describe('ChartTable', () => {
+  const mockedProps = {
+    user: {
+      userId: '2',
+    },
+  };
+  const wrapper = mount(<ChartTable {...mockedProps} />, {
+    context: { store },
+  });
+  it('it renders', () => {
+    expect(wrapper.find(ChartTable)).toExist();
+  });
+
+  it('fetches chart favorites and renders chart cards ', async () => {
+    expect(fetchMock.calls(chartsEndpoint)).toHaveLength(1);
+    await waitForComponentToPaint(wrapper);
+    expect(wrapper.find('ChartCard')).toExist();
+  });
+
+  it('display EmptyState if there is no data', () => {
+    fetchMock.resetHistory();
+    const wrapper = mount(<ChartTable {...mockedProps} />, {
+      context: { store },
+    });
+    expect(wrapper.find('EmptyState')).toExist();
+  });
+});
diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx
index e09b5fe..e7f28f3 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/DashboardTable_spec.tsx
@@ -17,48 +17,78 @@
  * under the License.
  */
 import React from 'react';
-import { mount } from 'enzyme';
+import { styledMount as mount } from 'spec/helpers/theming';
 import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
-import { supersetTheme, ThemeProvider } from '@superset-ui/core';
+import { act } from 'react-dom/test-utils';
 
-import ListView from 'src/components/ListView';
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import SubMenu from 'src/components/Menu/SubMenu';
 import DashboardTable from 'src/views/CRUD/welcome/DashboardTable';
+import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard';
 
 // store needed for withToasts(DashboardTable)
 const mockStore = configureStore([thunk]);
 const store = mockStore({});
 
-const dashboardsEndpoint = 'glob:*/api/v1/dashboard/*';
-const mockDashboards = [{ id: 1, url: 'url', dashboard_title: 'title' }];
+const dashboardsEndpoint = 'glob:*/api/v1/dashboard/?*';
+const dashboardInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
+const mockDashboards = [
+  {
+    id: 1,
+    url: 'url',
+    dashboard_title: 'title',
+    changed_on_utc: '24 Feb 2014 10:13:14',
+  },
+];
 
 fetchMock.get(dashboardsEndpoint, { result: mockDashboards });
+fetchMock.get(dashboardInfoEndpoint, {
+  permissions: ['can_list', 'can_edit', 'can_delete'],
+});
 
-function setup() {
-  // use mount because data fetching is triggered on mount
-  return mount(<DashboardTable />, {
+describe('DashboardTable', () => {
+  const dashboardProps = {
+    dashboardFilter: 'Favorite',
+    user: {
+      userId: '2',
+    },
+  };
+  const wrapper = mount(<DashboardTable {...dashboardProps} />, {
     context: { store },
-    wrappingComponent: ThemeProvider,
-    wrappingComponentProps: { theme: supersetTheme },
   });
-}
 
-describe('DashboardTable', () => {
-  beforeEach(fetchMock.resetHistory);
+  beforeAll(async () => {
+    await waitForComponentToPaint(wrapper);
+  });
+
+  it('renders', () => {
+    expect(wrapper.find(DashboardTable)).toExist();
+  });
 
-  it('fetches dashboards and renders a ListView', () => {
-    return new Promise(done => {
-      const wrapper = setup();
+  it('render a submenu with clickable tabs and buttons', async () => {
+    expect(wrapper.find(SubMenu)).toExist();
+    expect(wrapper.find('MenuItem')).toHaveLength(2);
+    expect(wrapper.find('Button')).toHaveLength(4);
+    act(() => {
+      wrapper.find('MenuItem').at(1).simulate('click');
+    });
+    await waitForComponentToPaint(wrapper);
+    expect(fetchMock.calls(/dashboard\/\?q/)).toHaveLength(1);
+  });
+
+  it('fetches dashboards and renders a card', () => {
+    expect(fetchMock.calls(/dashboard\/\?q/)).toHaveLength(1);
+    wrapper.setState({ dashboards: mockDashboards });
+    expect(wrapper.find(DashboardCard)).toExist();
+  });
 
-      setTimeout(() => {
-        expect(fetchMock.calls(dashboardsEndpoint)).toHaveLength(1);
-        // there's a delay between response and updating state, so manually set it
-        // rather than adding a timeout which could introduce flakiness
-        wrapper.setState({ dashboards: mockDashboards });
-        expect(wrapper.find(ListView)).toExist();
-        done();
-      });
+  it('display EmptyState if there is no data', () => {
+    fetchMock.resetHistory();
+    const wrapper = mount(<DashboardTable {...dashboardProps} />, {
+      context: { store },
     });
+    expect(wrapper.find('EmptyState')).toExist();
   });
 });
diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/EmptyState_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/EmptyState_spec.tsx
new file mode 100644
index 0000000..96ec1ec
--- /dev/null
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/EmptyState_spec.tsx
@@ -0,0 +1,92 @@
+/**
+ * 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 { styledMount as mount } from 'spec/helpers/theming';
+import EmptyState from 'src/views/CRUD/welcome/EmptyState';
+
+describe('EmptyState', () => {
+  const variants = [
+    {
+      tab: 'Favorite',
+      tableName: 'DASHBOARDS',
+    },
+    {
+      tab: 'Mine',
+      tableName: 'DASHBOARDS',
+    },
+    {
+      tab: 'Favorite',
+      tableName: 'CHARTS',
+    },
+    {
+      tab: 'Mine',
+      tableName: 'CHARTS',
+    },
+    {
+      tab: 'Favorite',
+      tableName: 'SAVED_QUERIES',
+    },
+    {
+      tab: 'Mine',
+      tableName: 'SAVED_QUEREIS',
+    },
+  ];
+  const recents = [
+    {
+      tab: 'Viewed',
+      tableName: 'RECENTS',
+    },
+    {
+      tab: 'Edited',
+      tableName: 'RECENTS',
+    },
+    {
+      tab: 'Created',
+      tableName: 'RECENTS',
+    },
+  ];
+  variants.forEach(variant => {
+    it(`it renders an ${variant.tab} ${variant.tableName} empty state`, () => {
+      const wrapper = mount(<EmptyState {...variant} />);
+      expect(wrapper).toExist();
+      const textContainer = wrapper.find('.ant-empty-description');
+      expect(textContainer.text()).toEqual(
+        variant.tab === 'Favorite'
+          ? "You don't have any favorites yet!"
+          : `No ${
+              variant.tableName === 'SAVED_QUERIES'
+                ? 'saved queries'
+                : variant.tableName.toLowerCase()
+            } yet`,
+      );
+      expect(wrapper.find('button')).toHaveLength(1);
+    });
+  });
+  recents.forEach(recent => {
+    it(`it renders an ${recent.tab} ${recent.tableName} empty state`, () => {
+      const wrapper = mount(<EmptyState {...recent} />);
+      expect(wrapper).toExist();
+      const textContainer = wrapper.find('.ant-empty-description');
+      expect(wrapper.find('.ant-empty-image').children()).toHaveLength(1);
+      expect(textContainer.text()).toContain(
+        `Recently ${recent.tab.toLowerCase()} charts, dashboards, and saved queries will appear here`,
+      );
+    });
+  });
+});
diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx
new file mode 100644
index 0000000..2670482
--- /dev/null
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx
@@ -0,0 +1,106 @@
+/**
+ * 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 thunk from 'redux-thunk';
+import { styledMount as mount } from 'spec/helpers/theming';
+import fetchMock from 'fetch-mock';
+import configureStore from 'redux-mock-store';
+import { act } from 'react-dom/test-utils';
+
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import SubMenu from 'src/components/Menu/SubMenu';
+import SavedQueries from 'src/views/CRUD/welcome/SavedQueries';
+
+// store needed for withToasts(DashboardTable)
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
+const queriesEndpoint = 'glob:*/api/v1/saved_query/?*';
+const savedQueriesInfo = 'glob:*/api/v1/saved_query/_info';
+
+const mockqueries = [...new Array(3)].map((_, i) => ({
+  created_by: {
+    id: i,
+    first_name: `user`,
+    last_name: `${i}`,
+  },
+  created_on: `${i}-2020`,
+  database: {
+    database_name: `db ${i}`,
+    id: i,
+  },
+  changed_on_delta_humanized: '1 day ago',
+  db_id: i,
+  description: `SQL for ${i}`,
+  id: i,
+  label: `query ${i}`,
+  schema: 'public',
+  sql: `SELECT ${i} FROM table`,
+  sql_tables: [
+    {
+      catalog: null,
+      schema: null,
+      table: `${i}`,
+    },
+  ],
+}));
+
+fetchMock.get(queriesEndpoint, {
+  result: mockqueries,
+});
+
+fetchMock.get(savedQueriesInfo, {
+  permissions: ['can_list', 'can_edit', 'can_delete'],
+});
+
+describe('SavedQueries', () => {
+  const savedQueryProps = {
+    user: {
+      userId: '1',
+    },
+  };
+
+  const wrapper = mount(<SavedQueries {...savedQueryProps} />, {
+    context: { store },
+  });
+  beforeAll(async () => {
+    await waitForComponentToPaint(wrapper);
+  });
+
+  it('is valid', () => {
+    expect(wrapper.find(SavedQueries)).toExist();
+  });
+
+  it('it renders a submenu with clickable tables and buttons', async () => {
+    expect(wrapper.find(SubMenu)).toExist();
+    expect(wrapper.find('MenuItem')).toHaveLength(2);
+    expect(wrapper.find('button')).toHaveLength(2);
+    act(() => {
+      wrapper.find('MenuItem').at(1).simulate('click');
+    });
+
+    await waitForComponentToPaint(wrapper);
+    expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(1);
+  });
+
+  it('fetches queries favorites and renders listviewcard cards', () => {
+    expect(fetchMock.calls(/saved_query\/\?q/)).toHaveLength(1);
+    expect(wrapper.find('ListViewCard')).toExist();
+  });
+});
diff --git a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
index bf23ef1..4cd051c 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
@@ -17,11 +17,14 @@
  * under the License.
  */
 import React from 'react';
-import { Panel, Row, Tab } from 'react-bootstrap';
 import { shallow } from 'enzyme';
-
+import thunk from 'redux-thunk';
+import configureStore from 'redux-mock-store';
 import Welcome from 'src/views/CRUD/welcome/Welcome';
 
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
+
 describe('Welcome', () => {
   const mockedProps = {
     user: {
@@ -34,13 +37,15 @@ describe('Welcome', () => {
       isActive: true,
     },
   };
-  it('is valid', () => {
-    expect(React.isValidElement(<Welcome {...mockedProps} />)).toBe(true);
+  const wrapper = shallow(<Welcome {...mockedProps} />, {
+    context: { store },
   });
-  it('renders 3 Tab, Panel, and Row components', () => {
-    const wrapper = shallow(<Welcome {...mockedProps} />);
-    expect(wrapper.find(Tab)).toHaveLength(3);
-    expect(wrapper.find(Panel)).toHaveLength(3);
-    expect(wrapper.find(Row)).toHaveLength(3);
+
+  it('renders', () => {
+    expect(wrapper).toExist();
+  });
+
+  it('renders all panels on the page on page load', () => {
+    expect(wrapper.find('CollapsePanel')).toHaveLength(4);
   });
 });
diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx
index 4a0cf53..8849b4b 100644
--- a/superset-frontend/src/components/ListViewCard/index.tsx
+++ b/superset-frontend/src/components/ListViewCard/index.tsx
@@ -143,15 +143,20 @@ const paragraphConfig = { rows: 1, width: 150 };
 interface CardProps {
   title: React.ReactNode;
   url?: string;
-  imgURL: string;
-  imgFallbackURL: string;
+  imgURL?: string;
+  imgFallbackURL?: string;
   imgPosition?: BackgroundPosition;
   description: string;
   loading: boolean;
   titleRight?: React.ReactNode;
   coverLeft?: React.ReactNode;
   coverRight?: React.ReactNode;
-  actions: React.ReactNode;
+  actions: React.ReactNode | null;
+  showImg?: boolean;
+  rows?: number | string;
+  avatar?: string;
+  isRecent?: boolean;
+  renderCover?: React.ReactNode | null;
 }
 
 function ListViewCard({
@@ -162,35 +167,42 @@ function ListViewCard({
   imgFallbackURL,
   description,
   coverLeft,
+  isRecent,
   coverRight,
   actions,
+  avatar,
   loading,
   imgPosition = 'top',
+  renderCover,
 }: CardProps) {
   return (
     <StyledCard
       data-test="styled-card"
       cover={
-        <Cover>
-          <a href={url}>
-            <div className="gradient-container">
-              <ImageLoader
-                src={imgURL}
-                fallback={imgFallbackURL}
-                isLoading={loading}
-                position={imgPosition}
-              />
-            </div>
-          </a>
-          <CoverFooter className="cover-footer">
-            {!loading && coverLeft && (
-              <CoverFooterLeft>{coverLeft}</CoverFooterLeft>
-            )}
-            {!loading && coverRight && (
-              <CoverFooterRight>{coverRight}</CoverFooterRight>
-            )}
-          </CoverFooter>
-        </Cover>
+        !isRecent
+          ? renderCover || (
+              <Cover>
+                <a href={url}>
+                  <div className="gradient-container">
+                    <ImageLoader
+                      src={imgURL || ''}
+                      fallback={imgFallbackURL || ''}
+                      isLoading={loading}
+                      position={imgPosition}
+                    />
+                  </div>
+                </a>
+                <CoverFooter className="cover-footer">
+                  {!loading && coverLeft && (
+                    <CoverFooterLeft>{coverLeft}</CoverFooterLeft>
+                  )}
+                  {!loading && coverRight && (
+                    <CoverFooterRight>{coverRight}</CoverFooterRight>
+                  )}
+                </CoverFooter>
+              </Cover>
+            )
+          : null
       }
     >
       {loading && (
@@ -230,6 +242,8 @@ function ListViewCard({
             </>
           }
           description={description}
+          // @ts-ignore
+          avatar={avatar ? <Icon name={avatar} /> : null}
         />
       )}
     </StyledCard>
diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx b/superset-frontend/src/components/Menu/SubMenu.tsx
index e17ce7a..bf626f1 100644
--- a/superset-frontend/src/components/Menu/SubMenu.tsx
+++ b/superset-frontend/src/components/Menu/SubMenu.tsx
@@ -53,10 +53,23 @@ const StyledHeader = styled.header`
     li.active > a,
     li.active > div,
     li > a:hover,
+    li > a:focus,
     li > div:hover {
-      background-color: ${({ theme }) => theme.colors.secondary.light4};
+      background: ${({ theme }) => theme.colors.secondary.light4};
       border-bottom: none;
-      border-radius: 4px;
+      border-radius: ${({ theme }) => theme.borderRadius}px;
+      margin-bottom: ${({ theme }) => theme.gridUnit * 2}px;
+    }
+  }
+  .navbar-inverse {
+    .navbar-nav {
+      & > .active > a {
+        background: ${({ theme }) => theme.colors.secondary.light4};
+        &:hover,
+        &:focus {
+          background: ${({ theme }) => theme.colors.secondary.light4};
+        }
+      }
     }
   }
 `;
@@ -64,8 +77,9 @@ const StyledHeader = styled.header`
 type MenuChild = {
   label: string;
   name: string;
-  url: string;
+  url?: string;
   usesRouter?: boolean;
+  onClick?: () => void;
 };
 
 export interface ButtonProps {
@@ -83,8 +97,8 @@ export interface ButtonProps {
 
 export interface SubMenuProps {
   buttons?: Array<ButtonProps>;
-  name: string;
-  children?: MenuChild[];
+  name?: string;
+  tabs?: MenuChild[];
   activeChild?: MenuChild['name'];
   /* If usesRouter is true, a react-router <Link> component will be used instead of href.
    *  ONLY set usesRouter to true if SubMenu is wrapped in a react-router <Router>;
@@ -108,16 +122,16 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
           <Navbar.Brand>{props.name}</Navbar.Brand>
         </Navbar.Header>
         <Nav>
-          {props.children &&
-            props.children.map(child => {
-              if ((props.usesRouter || hasHistory) && !!child.usesRouter) {
+          {props.tabs &&
+            props.tabs.map(tab => {
+              if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
                 return (
                   <li
-                    className={child.name === props.activeChild ? 'active' : ''}
-                    key={`${child.label}`}
+                    className={tab.name === props.activeChild ? 'active' : ''}
+                    key={`${tab.label}`}
                   >
                     <div>
-                      <Link to={child.url}>{child.label}</Link>
+                      <Link to={tab.url || ''}>{tab.label}</Link>
                     </div>
                   </li>
                 );
@@ -126,11 +140,12 @@ const SubMenu: React.FunctionComponent<SubMenuProps> = props => {
               return (
                 <MenuItem
                   className="no-router"
-                  active={child.name === props.activeChild}
-                  key={`${child.label}`}
-                  href={child.url}
+                  active={tab.name === props.activeChild}
+                  key={`${tab.label}`}
+                  href={tab.url}
+                  onClick={tab.onClick}
                 >
-                  {child.label}
+                  {tab.label}
                 </MenuItem>
               );
             })}
diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
new file mode 100644
index 0000000..5dd6bd8
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
@@ -0,0 +1,138 @@
+/**
+ * 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 { useFavoriteStatus } from 'src/views/CRUD/hooks';
+import { t } from '@superset-ui/core';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+import Icon from 'src/components/Icon';
+import Chart from 'src/types/Chart';
+
+import ListViewCard from 'src/components/ListViewCard';
+import Label from 'src/components/Label';
+import { Dropdown, Menu } from 'src/common/components';
+import FaveStar from 'src/components/FaveStar';
+import FacePile from 'src/components/FacePile';
+import { handleChartDelete } from '../utils';
+
+const FAVESTAR_BASE_URL = '/superset/favstar/slice';
+
+interface ChartCardProps {
+  chart: Chart;
+  hasPerm: (perm: string) => boolean;
+  openChartEditModal: (chart: Chart) => void;
+  bulkSelectEnabled: boolean;
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+  refreshData: () => void;
+  loading: boolean;
+}
+
+export default function ChartCard({
+  chart,
+  hasPerm,
+  openChartEditModal,
+  bulkSelectEnabled,
+  addDangerToast,
+  addSuccessToast,
+  refreshData,
+  loading,
+}: ChartCardProps) {
+  const canEdit = hasPerm('can_edit');
+  const canDelete = hasPerm('can_delete');
+  const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
+    {},
+    FAVESTAR_BASE_URL,
+    addDangerToast,
+  );
+
+  const menu = (
+    <Menu>
+      {canDelete && (
+        <Menu.Item>
+          <ConfirmStatusChange
+            title={t('Please Confirm')}
+            description={
+              <>
+                {t('Are you sure you want to delete')} <b>{chart.slice_name}</b>
+                ?
+              </>
+            }
+            onConfirm={() =>
+              handleChartDelete(
+                chart,
+                addSuccessToast,
+                addDangerToast,
+                refreshData,
+              )
+            }
+          >
+            {confirmDelete => (
+              <div
+                data-test="chart-list-delete-option"
+                role="button"
+                tabIndex={0}
+                className="action-button"
+                onClick={confirmDelete}
+              >
+                <ListViewCard.MenuIcon name="trash" /> {t('Delete')}
+              </div>
+            )}
+          </ConfirmStatusChange>
+        </Menu.Item>
+      )}
+      {canEdit && (
+        <Menu.Item
+          data-test="chart-list-edit-option"
+          role="button"
+          tabIndex={0}
+          onClick={() => openChartEditModal(chart)}
+        >
+          <ListViewCard.MenuIcon name="edit-alt" /> {t('Edit')}
+        </Menu.Item>
+      )}
+    </Menu>
+  );
+  return (
+    <ListViewCard
+      loading={loading}
+      title={chart.slice_name}
+      url={bulkSelectEnabled ? undefined : chart.url}
+      imgURL={chart.thumbnail_url || ''}
+      imgFallbackURL="/static/assets/images/chart-card-fallback.png"
+      description={t('Last modified %s', chart.changed_on_delta_humanized)}
+      coverLeft={<FacePile users={chart.owners || []} />}
+      coverRight={
+        <Label bsStyle="secondary">{chart.datasource_name_text}</Label>
+      }
+      actions={
+        <ListViewCard.Actions>
+          <FaveStar
+            itemId={chart.id}
+            fetchFaveStar={fetchFaveStar}
+            saveFaveStar={saveFaveStar}
+            isStarred={!!favoriteStatus[chart.id]}
+          />
+          <Dropdown overlay={menu}>
+            <Icon name="more-horiz" />
+          </Dropdown>
+        </ListViewCard.Actions>
+      }
+    />
+  );
+}
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index a22c54d..91cb458 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -17,15 +17,22 @@
  * under the License.
  */
 import { SupersetClient, getChartMetadataRegistry, t } from '@superset-ui/core';
-import React, { useState, useMemo } from 'react';
+import React, { useMemo } from 'react';
 import rison from 'rison';
 import { uniqBy } from 'lodash';
 import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
-import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
-import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
+import {
+  createFetchRelated,
+  createErrorHandler,
+  handleChartDelete,
+} from 'src/views/CRUD/utils';
+import {
+  useListViewResource,
+  useFavoriteStatus,
+  useChartEditModal,
+} from 'src/views/CRUD/hooks';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
-import FacePile from 'src/components/FacePile';
 import Icon from 'src/components/Icon';
 import FaveStar from 'src/components/FaveStar';
 import ListView, {
@@ -35,11 +42,9 @@ import ListView, {
 } from 'src/components/ListView';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import PropertiesModal from 'src/explore/components/PropertiesModal';
-import Chart, { Slice } from 'src/types/Chart';
-import ListViewCard from 'src/components/ListViewCard';
-import Label from 'src/components/Label';
-import { Dropdown, Menu } from 'src/common/components';
+import Chart from 'src/types/Chart';
 import TooltipWrapper from 'src/components/TooltipWrapper';
+import ChartCard from './ChartCard';
 
 const PAGE_SIZE = 25;
 const FAVESTAR_BASE_URL = '/superset/favstar/slice';
@@ -105,51 +110,18 @@ function ChartList(props: ChartListProps) {
     FAVESTAR_BASE_URL,
     props.addDangerToast,
   );
-  const [
+  const {
     sliceCurrentlyEditing,
-    setSliceCurrentlyEditing,
-  ] = useState<Slice | null>(null);
+    handleChartUpdated,
+    openChartEditModal,
+    closeChartEditModal,
+  } = useChartEditModal(setCharts, charts);
 
   const canCreate = hasPerm('can_add');
   const canEdit = hasPerm('can_edit');
   const canDelete = hasPerm('can_delete');
   const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
 
-  function openChartEditModal(chart: Chart) {
-    setSliceCurrentlyEditing({
-      slice_id: chart.id,
-      slice_name: chart.slice_name,
-      description: chart.description,
-      cache_timeout: chart.cache_timeout,
-    });
-  }
-
-  function closeChartEditModal() {
-    setSliceCurrentlyEditing(null);
-  }
-
-  function handleChartUpdated(edits: Chart) {
-    // update the chart in our state with the edited info
-    const newCharts = charts.map(chart =>
-      chart.id === edits.id ? { ...chart, ...edits } : chart,
-    );
-    setCharts(newCharts);
-  }
-
-  function handleChartDelete({ id, slice_name: sliceName }: Chart) {
-    SupersetClient.delete({
-      endpoint: `/api/v1/chart/${id}`,
-    }).then(
-      () => {
-        refreshData();
-        props.addSuccessToast(t('Deleted: %s', sliceName));
-      },
-      () => {
-        props.addDangerToast(t('There was an issue deleting: %s', sliceName));
-      },
-    );
-  }
-
   function handleBulkChartDelete(chartsToDelete: Chart[]) {
     SupersetClient.delete({
       endpoint: `/api/v1/chart/?q=${rison.encode(
@@ -266,7 +238,13 @@ function ChartList(props: ChartListProps) {
       },
       {
         Cell: ({ row: { original } }: any) => {
-          const handleDelete = () => handleChartDelete(original);
+          const handleDelete = () =>
+            handleChartDelete(
+              original,
+              props.addSuccessToast,
+              props.addDangerToast,
+              refreshData,
+            );
           const openEditModal = () => openChartEditModal(original);
 
           return (
@@ -426,69 +404,17 @@ function ChartList(props: ChartListProps) {
     },
   ];
 
-  function renderCard(chart: Chart & { loading: boolean }) {
-    const menu = (
-      <Menu>
-        {canDelete && (
-          <Menu.Item>
-            <ConfirmStatusChange
-              title={t('Please Confirm')}
-              description={
-                <>
-                  {t('Are you sure you want to delete')}{' '}
-                  <b>{chart.slice_name}</b>?
-                </>
-              }
-              onConfirm={() => handleChartDelete(chart)}
-            >
-              {confirmDelete => (
-                <div
-                  data-test="chart-list-delete-option"
-                  role="button"
-                  tabIndex={0}
-                  className="action-button"
-                  onClick={confirmDelete}
-                >
-                  <ListViewCard.MenuIcon name="trash" /> Delete
-                </div>
-              )}
-            </ConfirmStatusChange>
-          </Menu.Item>
-        )}
-        {canEdit && (
-          <Menu.Item
-            data-test="chart-list-edit-option"
-            role="button"
-            tabIndex={0}
-            onClick={() => openChartEditModal(chart)}
-          >
-            <ListViewCard.MenuIcon name="edit-alt" /> Edit
-          </Menu.Item>
-        )}
-      </Menu>
-    );
-
+  function renderCard(chart: Chart) {
     return (
-      <ListViewCard
-        loading={chart.loading}
-        title={chart.slice_name}
-        url={bulkSelectEnabled ? undefined : chart.url}
-        imgURL={chart.thumbnail_url ?? ''}
-        imgFallbackURL="/static/assets/images/chart-card-fallback.png"
-        imgPosition="bottom"
-        description={t('Last modified %s', chart.changed_on_delta_humanized)}
-        coverLeft={<FacePile users={chart.owners || []} />}
-        coverRight={
-          <Label bsStyle="secondary">{chart.datasource_name_text}</Label>
-        }
-        actions={
-          <ListViewCard.Actions>
-            {renderFaveStar(chart.id)}
-            <Dropdown data-test="dropdown-options" overlay={menu}>
-              <Icon name="more-horiz" />
-            </Dropdown>
-          </ListViewCard.Actions>
-        }
+      <ChartCard
+        chart={chart}
+        hasPerm={hasPerm}
+        openChartEditModal={openChartEditModal}
+        bulkSelectEnabled={bulkSelectEnabled}
+        addDangerToast={props.addDangerToast}
+        addSuccessToast={props.addSuccessToast}
+        refreshData={refreshData}
+        loading={loading}
       />
     );
   }
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
new file mode 100644
index 0000000..f8713d6
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
@@ -0,0 +1,140 @@
+/**
+ * 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 { t } from '@superset-ui/core';
+import {
+  handleDashboardDelete,
+  handleBulkDashboardExport,
+} from 'src/views/CRUD/utils';
+import { Dropdown, Menu } from 'src/common/components';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
+import ListViewCard from 'src/components/ListViewCard';
+import Icon from 'src/components/Icon';
+import Label from 'src/components/Label';
+import FacePile from 'src/components/FacePile';
+import FaveStar from 'src/components/FaveStar';
+import { DashboardCardProps } from 'src/views/CRUD/types';
+
+import { useFavoriteStatus } from 'src/views/CRUD/hooks';
+
+const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
+
+function DashboardCard({
+  dashboard,
+  hasPerm,
+  bulkSelectEnabled,
+  refreshData,
+  addDangerToast,
+  addSuccessToast,
+  openDashboardEditModal,
+}: DashboardCardProps) {
+  const canEdit = hasPerm('can_edit');
+  const canDelete = hasPerm('can_delete');
+  const canExport = hasPerm('can_mulexport');
+  const [, fetchFaveStar, saveFaveStar, favoriteStatus] = useFavoriteStatus(
+    {},
+    FAVESTAR_BASE_URL,
+    addDangerToast,
+  );
+
+  const menu = (
+    <Menu>
+      {canEdit && openDashboardEditModal && (
+        <Menu.Item
+          role="button"
+          tabIndex={0}
+          onClick={() =>
+            openDashboardEditModal && openDashboardEditModal(dashboard)
+          }
+        >
+          <ListViewCard.MenuIcon name="edit-alt" /> Edit
+        </Menu.Item>
+      )}
+      {canExport && (
+        <Menu.Item
+          role="button"
+          tabIndex={0}
+          onClick={() => handleBulkDashboardExport([dashboard])}
+        >
+          <ListViewCard.MenuIcon name="share" /> Export
+        </Menu.Item>
+      )}
+      {canDelete && (
+        <Menu.Item>
+          <ConfirmStatusChange
+            title={t('Please Confirm')}
+            description={
+              <>
+                {t('Are you sure you want to delete')}{' '}
+                <b>{dashboard.dashboard_title}</b>?
+              </>
+            }
+            onConfirm={() =>
+              handleDashboardDelete(
+                dashboard,
+                refreshData,
+                addSuccessToast,
+                addDangerToast,
+              )
+            }
+          >
+            {confirmDelete => (
+              <div
+                role="button"
+                tabIndex={0}
+                className="action-button"
+                onClick={confirmDelete}
+              >
+                <ListViewCard.MenuIcon name="trash" /> Delete
+              </div>
+            )}
+          </ConfirmStatusChange>
+        </Menu.Item>
+      )}
+    </Menu>
+  );
+  return (
+    <ListViewCard
+      loading={dashboard.loading || false}
+      title={dashboard.dashboard_title}
+      titleRight={<Label>{dashboard.published ? 'published' : 'draft'}</Label>}
+      url={bulkSelectEnabled ? undefined : dashboard.url}
+      imgURL={dashboard.thumbnail_url}
+      imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
+      description={t('Last modified %s', dashboard.changed_on_delta_humanized)}
+      coverLeft={<FacePile users={dashboard.owners || []} />}
+      actions={
+        <ListViewCard.Actions>
+          <FaveStar
+            itemId={dashboard.id}
+            fetchFaveStar={fetchFaveStar}
+            saveFaveStar={saveFaveStar}
+            isStarred={!!favoriteStatus[dashboard.id]}
+          />
+          <Dropdown overlay={menu}>
+            <Icon name="more-horiz" />
+          </Dropdown>
+        </ListViewCard.Actions>
+      }
+      showImg
+    />
+  );
+}
+
+export default DashboardCard;
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index ac4d747..d8acca3 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -20,22 +20,27 @@ import { SupersetClient, t } from '@superset-ui/core';
 import React, { useState, useMemo } from 'react';
 import rison from 'rison';
 import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
-import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import {
+  createFetchRelated,
+  createErrorHandler,
+  handleDashboardDelete,
+  handleBulkDashboardExport,
+} from 'src/views/CRUD/utils';
 import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
-import FacePile from 'src/components/FacePile';
 import ListView, { ListViewProps, Filters } from 'src/components/ListView';
 import Owner from 'src/types/Owner';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
+import FacePile from 'src/components/FacePile';
 import Icon from 'src/components/Icon';
-import Label from 'src/components/Label';
 import FaveStar from 'src/components/FaveStar';
 import PropertiesModal from 'src/dashboard/components/PropertiesModal';
-import ListViewCard from 'src/components/ListViewCard';
-import { Dropdown, Menu } from 'src/common/components';
 import TooltipWrapper from 'src/components/TooltipWrapper';
 
+import Dashboard from 'src/dashboard/containers/Dashboard';
+import DashboardCard from './DashboardCard';
+
 const PAGE_SIZE = 25;
 const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
 
@@ -81,7 +86,6 @@ function DashboardList(props: DashboardListProps) {
     FAVESTAR_BASE_URL,
     props.addDangerToast,
   );
-
   const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
     null,
   );
@@ -119,25 +123,6 @@ function DashboardList(props: DashboardListProps) {
     );
   }
 
-  function handleDashboardDelete({
-    id,
-    dashboard_title: dashboardTitle,
-  }: Dashboard) {
-    return SupersetClient.delete({
-      endpoint: `/api/v1/dashboard/${id}`,
-    }).then(
-      () => {
-        refreshData();
-        props.addSuccessToast(t('Deleted: %s', dashboardTitle));
-      },
-      createErrorHandler(errMsg =>
-        props.addDangerToast(
-          t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
-        ),
-      ),
-    );
-  }
-
   function handleBulkDashboardDelete(dashboardsToDelete: Dashboard[]) {
     return SupersetClient.delete({
       endpoint: `/api/v1/dashboard/?q=${rison.encode(
@@ -155,14 +140,6 @@ function DashboardList(props: DashboardListProps) {
     );
   }
 
-  function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
-    return window.location.assign(
-      `/api/v1/dashboard/export/?q=${rison.encode(
-        dashboardsToExport.map(({ id }) => id),
-      )}`,
-    );
-  }
-
   function renderFaveStar(id: number) {
     return (
       <FaveStar
@@ -255,7 +232,13 @@ function DashboardList(props: DashboardListProps) {
       },
       {
         Cell: ({ row: { original } }: any) => {
-          const handleDelete = () => handleDashboardDelete(original);
+          const handleDelete = () =>
+            handleDashboardDelete(
+              original,
+              refreshData,
+              props.addSuccessToast,
+              props.addDangerToast,
+            );
           const handleEdit = () => openDashboardEditModal(original);
           const handleExport = () => handleBulkDashboardExport([original]);
 
@@ -418,83 +401,18 @@ function DashboardList(props: DashboardListProps) {
     },
   ];
 
-  function renderCard(dashboard: Dashboard & { loading: boolean }) {
-    const menu = (
-      <Menu>
-        {canDelete && (
-          <Menu.Item>
-            <ConfirmStatusChange
-              title={t('Please Confirm')}
-              description={
-                <>
-                  {t('Are you sure you want to delete')}{' '}
-                  <b>{dashboard.dashboard_title}</b>?
-                </>
-              }
-              onConfirm={() => handleDashboardDelete(dashboard)}
-            >
-              {confirmDelete => (
-                <div
-                  role="button"
-                  tabIndex={0}
-                  className="action-button"
-                  onClick={confirmDelete}
-                >
-                  <ListViewCard.MenuIcon
-                    data-test="dashboard-list-view-card-trash-icon"
-                    name="trash"
-                  />{' '}
-                  Delete
-                </div>
-              )}
-            </ConfirmStatusChange>
-          </Menu.Item>
-        )}
-        {canExport && (
-          <Menu.Item
-            role="button"
-            tabIndex={0}
-            onClick={() => handleBulkDashboardExport([dashboard])}
-          >
-            <ListViewCard.MenuIcon name="share" /> Export
-          </Menu.Item>
-        )}
-        {canEdit && (
-          <Menu.Item
-            data-test="dashboard-list-edit-option"
-            role="button"
-            tabIndex={0}
-            onClick={() => openDashboardEditModal(dashboard)}
-          >
-            <ListViewCard.MenuIcon name="edit-alt" /> Edit
-          </Menu.Item>
-        )}
-      </Menu>
-    );
-
+  function renderCard(dashboard: Dashboard) {
     return (
-      <ListViewCard
-        loading={dashboard.loading}
-        title={dashboard.dashboard_title}
-        titleRight={
-          <Label>{dashboard.published ? 'published' : 'draft'}</Label>
-        }
-        url={bulkSelectEnabled ? undefined : dashboard.url}
-        imgURL={dashboard.thumbnail_url}
-        imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
-        description={t(
-          'Last modified %s',
-          dashboard.changed_on_delta_humanized,
-        )}
-        coverLeft={<FacePile users={dashboard.owners || []} />}
-        actions={
-          <ListViewCard.Actions>
-            {renderFaveStar(dashboard.id)}
-            <Dropdown overlay={menu}>
-              <Icon name="more-horiz" />
-            </Dropdown>
-          </ListViewCard.Actions>
-        }
+      <DashboardCard
+        {...{
+          dashboard,
+          hasPerm,
+          bulkSelectEnabled,
+          refreshData,
+          addDangerToast: props.addDangerToast,
+          addSuccessToast: props.addSuccessToast,
+          openDashboardEditModal,
+        }}
       />
     );
   }
diff --git a/superset-frontend/src/views/CRUD/data/common.ts b/superset-frontend/src/views/CRUD/data/common.ts
index 9b2194a..6fecd21 100644
--- a/superset-frontend/src/views/CRUD/data/common.ts
+++ b/superset-frontend/src/views/CRUD/data/common.ts
@@ -20,7 +20,7 @@ import { t } from '@superset-ui/core';
 
 export const commonMenuData = {
   name: t('Data'),
-  children: [
+  tabs: [
     {
       name: 'Datasets',
       label: t('Datasets'),
diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
index f887af5..bfb4719 100644
--- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
+++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx
@@ -39,6 +39,7 @@ import DeleteModal from 'src/components/DeleteModal';
 import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar';
 import { IconName } from 'src/components/Icon';
 import { commonMenuData } from 'src/views/CRUD/data/common';
+import { SavedQueryObject } from 'src/views/CRUD/types';
 import SavedQueryPreviewModal from './SavedQueryPreviewModal';
 
 const PAGE_SIZE = 25;
@@ -48,20 +49,6 @@ interface SavedQueryListProps {
   addSuccessToast: (msg: string) => void;
 }
 
-type SavedQueryObject = {
-  database: {
-    database_name: string;
-    id: number;
-  };
-  db_id: number;
-  description?: string;
-  id: number;
-  label: string;
-  schema: string;
-  sql: string;
-  sql_tables: Array<{ catalog?: string; schema: string; table: string }>;
-};
-
 const StyledTableLabel = styled.div`
   .count {
     margin-left: 5px;
diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts
index 9c2386f..b083bde 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -22,6 +22,7 @@ import { SupersetClient, t } from '@superset-ui/core';
 
 import { createErrorHandler } from 'src/views/CRUD/utils';
 import { FetchDataConfig } from 'src/components/ListView';
+import Chart, { Slice } from 'src/types/Chart';
 import { FavoriteStatus } from './types';
 
 interface ListViewResourceState<D extends object = any> {
@@ -350,5 +351,87 @@ export function useFavoriteStatus(
     );
   };
 
-  return [favoriteStatusRef, fetchFaveStar, saveFaveStar] as const;
+  return [
+    favoriteStatusRef,
+    fetchFaveStar,
+    saveFaveStar,
+    favoriteStatus,
+  ] as const;
 }
+
+export const useChartEditModal = (
+  setCharts: (charts: Array<Chart>) => void,
+  charts: Array<Chart>,
+) => {
+  const [
+    sliceCurrentlyEditing,
+    setSliceCurrentlyEditing,
+  ] = useState<Slice | null>(null);
+
+  function openChartEditModal(chart: Chart) {
+    setSliceCurrentlyEditing({
+      slice_id: chart.id,
+      slice_name: chart.slice_name,
+      description: chart.description,
+      cache_timeout: chart.cache_timeout,
+    });
+  }
+
+  function closeChartEditModal() {
+    setSliceCurrentlyEditing(null);
+  }
+
+  function handleChartUpdated(edits: Chart) {
+    // update the chart in our state with the edited info
+    const newCharts = charts.map((chart: Chart) =>
+      chart.id === edits.id ? { ...chart, ...edits } : chart,
+    );
+    setCharts(newCharts);
+  }
+
+  return {
+    sliceCurrentlyEditing,
+    handleChartUpdated,
+    openChartEditModal,
+    closeChartEditModal,
+  };
+};
+
+export const copyQueryLink = (
+  id: number,
+  addDangerToast: (arg0: string) => void,
+  addSuccessToast: (arg0: string) => void,
+) => {
+  const selection: Selection | null = document.getSelection();
+
+  if (selection) {
+    selection.removeAllRanges();
+    const range = document.createRange();
+    const span = document.createElement('span');
+    span.textContent = `${window.location.origin}/superset/sqllab?savedQueryId=${id}`;
+    span.style.position = 'fixed';
+    span.style.top = '0';
+    span.style.clip = 'rect(0, 0, 0, 0)';
+    span.style.whiteSpace = 'pre';
+
+    document.body.appendChild(span);
+    range.selectNode(span);
+    selection.addRange(range);
+
+    try {
+      if (!document.execCommand('copy')) {
+        throw new Error(t('Not successful'));
+      }
+    } catch (err) {
+      addDangerToast(t('Sorry, your browser does not support copying.'));
+    }
+
+    document.body.removeChild(span);
+    if (selection.removeRange) {
+      selection.removeRange(range);
+    } else {
+      selection.removeAllRanges();
+    }
+    addSuccessToast(t('Link Copied!'));
+  }
+};
diff --git a/superset-frontend/src/views/CRUD/types.ts b/superset-frontend/src/views/CRUD/types.ts
index 91d88a3..0946247 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -16,7 +16,56 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { User } from 'src/types/bootstrapTypes';
+import Owner from 'src/types/Owner';
 
 export type FavoriteStatus = {
   [id: number]: boolean;
 };
+
+export interface DashboardTableProps {
+  addDangerToast: (message: string) => void;
+  addSuccessToast: (message: string) => void;
+  search: string;
+  user?: User;
+}
+
+export interface Dashboard {
+  changed_by_name: string;
+  changed_by_url: string;
+  changed_on_delta_humanized: string;
+  changed_by: string;
+  dashboard_title: string;
+  slice_name?: string;
+  id: number;
+  published: boolean;
+  url: string;
+  thumbnail_url: string;
+  owners: Owner[];
+  loading?: boolean;
+}
+
+export interface DashboardCardProps {
+  isChart?: boolean;
+  dashboard: Dashboard;
+  hasPerm: (name: string) => boolean;
+  bulkSelectEnabled: boolean;
+  refreshData: () => void;
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+  openDashboardEditModal?: (d: Dashboard) => void;
+}
+
+export type SavedQueryObject = {
+  database: {
+    database_name: string;
+    id: number;
+  };
+  db_id: number;
+  description?: string;
+  id: number;
+  label: string;
+  schema: string;
+  sql: string;
+  sql_tables: Array<{ catalog?: string; schema: string; table: string }>;
+};
diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx
index 213481d..ee7cbeb 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -17,12 +17,16 @@
  * under the License.
  */
 import {
+  t,
   SupersetClient,
   SupersetClientResponse,
   logging,
+  styled,
 } from '@superset-ui/core';
+import Chart from 'src/types/Chart';
 import rison from 'rison';
 import getClientErrorObject from 'src/utils/getClientErrorObject';
+import { Dashboard } from './types';
 
 const createFetchResourceMethod = (method: string) => (
   resource: string,
@@ -53,6 +57,102 @@ const createFetchResourceMethod = (method: string) => (
   return [];
 };
 
+export const getRecentAcitivtyObjs = (
+  userId: string | number,
+  recent: string,
+  addDangerToast: (arg0: string, arg1: string) => void,
+) => {
+  const getParams = (filters?: Array<any>) => {
+    const params = {
+      order_column: 'changed_on_delta_humanized',
+      order_direction: 'desc',
+      page: 0,
+      page_size: 3,
+      filters,
+    };
+    if (!filters) delete params.filters;
+    return rison.encode(params);
+  };
+  const filters = {
+    // chart and dashbaord uses same filters
+    // for edited and created
+    edited: [
+      {
+        col: 'changed_by',
+        opr: 'rel_o_m',
+        value: `${userId}`,
+      },
+    ],
+    created: [
+      {
+        col: 'created_by',
+        opr: 'rel_o_m',
+        value: `${userId}`,
+      },
+    ],
+  };
+  const baseBatch = [
+    SupersetClient.get({ endpoint: recent }),
+    SupersetClient.get({
+      endpoint: `/api/v1/dashboard/?q=${getParams(filters.edited)}`,
+    }),
+    SupersetClient.get({
+      endpoint: `/api/v1/chart/?q=${getParams(filters.edited)}`,
+    }),
+    SupersetClient.get({
+      endpoint: `/api/v1/dashboard/?q=${getParams(filters.created)}`,
+    }),
+    SupersetClient.get({
+      endpoint: `/api/v1/chart/?q=${getParams(filters.created)}`,
+    }),
+    SupersetClient.get({
+      endpoint: `/api/v1/saved_query/?q=${getParams(filters.created)}`,
+    }),
+  ];
+  return Promise.all(baseBatch).then(
+    ([
+      recentsRes,
+      editedDash,
+      editedChart,
+      createdByDash,
+      createdByChart,
+      createdByQuery,
+    ]) => {
+      const res: any = {
+        editedDash: editedDash.json?.result.slice(0, 3),
+        editedChart: editedChart.json?.result.slice(0, 3),
+        createdByDash: createdByDash.json?.result.slice(0, 3),
+        createdByChart: createdByChart.json?.result.slice(0, 3),
+        createdByQuery: createdByQuery.json?.result.slice(0, 3),
+      };
+      if (recentsRes.json.length === 0) {
+        const newBatch = [
+          SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }),
+          SupersetClient.get({
+            endpoint: `/api/v1/dashboard/?q=${getParams()}`,
+          }),
+        ];
+        return Promise.all(newBatch)
+          .then(([chartRes, dashboardRes]) => {
+            res.examples = [
+              ...chartRes.json.result,
+              ...dashboardRes.json.result,
+            ];
+            return res;
+          })
+          .catch(e =>
+            addDangerToast(
+              'There was an error fetching you recent activity:',
+              e,
+            ),
+          );
+      }
+      res.viewed = recentsRes.json;
+      return res;
+    },
+  );
+};
+
 export const createFetchRelated = createFetchResourceMethod('related');
 export const createFetchDistinct = createFetchResourceMethod('distinct');
 
@@ -63,3 +163,100 @@ export function createErrorHandler(handleErrorFunc: (errMsg?: string) => void) {
     handleErrorFunc(parsedError.message || parsedError.error);
   };
 }
+
+export function handleChartDelete(
+  { id, slice_name: sliceName }: Chart,
+  addSuccessToast: (arg0: string) => void,
+  addDangerToast: (arg0: string) => void,
+  refreshData: () => void,
+) {
+  SupersetClient.delete({
+    endpoint: `/api/v1/chart/${id}`,
+  }).then(
+    () => {
+      refreshData();
+      addSuccessToast(t('Deleted: %s', sliceName));
+    },
+    () => {
+      addDangerToast(t('There was an issue deleting: %s', sliceName));
+    },
+  );
+}
+
+export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
+  return window.location.assign(
+    `/api/v1/dashboard/export/?q=${rison.encode(
+      dashboardsToExport.map(({ id }) => id),
+    )}`,
+  );
+}
+
+export function handleDashboardDelete(
+  { id, dashboard_title: dashboardTitle }: Dashboard,
+  refreshData: () => void,
+  addSuccessToast: (arg0: string) => void,
+  addDangerToast: (arg0: string) => void,
+) {
+  return SupersetClient.delete({
+    endpoint: `/api/v1/dashboard/${id}`,
+  }).then(
+    () => {
+      refreshData();
+      addSuccessToast(t('Deleted: %s', dashboardTitle));
+    },
+    createErrorHandler(errMsg =>
+      addDangerToast(
+        t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
+      ),
+    ),
+  );
+}
+
+export function createChartDeleteFunction(
+  { id, slice_name: sliceName }: Chart,
+  addSuccessToast: (arg0: string) => void,
+  addDangerToast: (arg0: string) => void,
+  refreshData: () => void,
+) {
+  SupersetClient.delete({
+    endpoint: `/api/v1/chart/${id}`,
+  }).then(
+    () => {
+      refreshData();
+      addSuccessToast(t('Deleted: %s', sliceName));
+    },
+    () => {
+      addDangerToast(t('There was an issue deleting: %s', sliceName));
+    },
+  );
+}
+
+const breakpoints = [576, 768, 992, 1200];
+export const mq = breakpoints.map(bp => `@media (max-width: ${bp}px)`);
+
+export const CardContainer = styled.div`
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
+  ${[mq[3]]} {
+    grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
+  }
+
+  ${[mq[2]]} {
+    grid-template-columns: repeat(auto-fit, minmax(48%, max-content));
+  }
+
+  ${[mq[1]]} {
+    grid-template-columns: repeat(auto-fit, minmax(50%, max-content));
+  }
+  grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
+  justify-content: left;
+  padding: ${({ theme }) => theme.gridUnit * 2}px
+    ${({ theme }) => theme.gridUnit * 6}px;
+`;
+
+export const IconContainer = styled.div`
+  svg {
+    vertical-align: -7px;
+    color: ${({ theme }) => theme.colors.primary.dark1};
+  }
+`;
diff --git a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
new file mode 100644
index 0000000..dc44347
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
@@ -0,0 +1,209 @@
+/**
+ * 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, { useEffect, useState } from 'react';
+import moment from 'antd/node_modules/moment';
+import { styled, t } from '@superset-ui/core';
+
+import ListViewCard from 'src/components/ListViewCard';
+import { addDangerToast } from 'src/messageToasts/actions';
+import SubMenu from 'src/components/Menu/SubMenu';
+import { reject } from 'lodash';
+import { getRecentAcitivtyObjs, mq } from '../utils';
+import EmptyState from './EmptyState';
+
+interface ActivityObjects {
+  action?: string;
+  item_title?: string;
+  slice_name: string;
+  time: string;
+  changed_on_utc: string;
+  url: string;
+  sql: string;
+  dashboard_title: string;
+  label: string;
+  id: string;
+  table: object;
+  item_url: string;
+}
+
+interface ActivityProps {
+  user: {
+    userId: string | number;
+  };
+}
+
+interface ActivityData {
+  Created?: Array<object>;
+  Edited?: Array<object>;
+  Viewed?: Array<object>;
+  Examples?: Array<object>;
+}
+
+const ActivityContainer = styled.div`
+  margin-left: ${({ theme }) => theme.gridUnit * 2}px;
+  margin-top: ${({ theme }) => theme.gridUnit * -4}px;
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
+  ${[mq[3]]} {
+    grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
+  }
+  ${[mq[2]]} {
+    grid-template-columns: repeat(auto-fit, minmax(42%, max-content));
+  }
+  ${[mq[1]]} {
+    grid-template-columns: repeat(auto-fit, minmax(63%, max-content));
+  }
+  grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
+  justify-content: left;
+  padding: ${({ theme }) => theme.gridUnit * 2}px
+    ${({ theme }) => theme.gridUnit * 4}px;
+  .ant-card-meta-avatar {
+    margin-top: ${({ theme }) => theme.gridUnit * 3}px;
+    margin-left: ${({ theme }) => theme.gridUnit * 2}px;
+  }
+  .ant-card-meta-title {
+    font-weight: ${({ theme }) => theme.typography.weights.bold};
+  }
+`;
+
+export default function ActivityTable({ user }: ActivityProps) {
+  const [activityData, setActivityData] = useState<ActivityData>({});
+  const [loading, setLoading] = useState(true);
+  const [activeChild, setActiveChild] = useState('Viewed');
+  // this api uses log for data which in some cases can be empty
+  const recent = `/superset/recent_activity/${user.userId}/?limit=5`;
+
+  const getFilterTitle = (e: ActivityObjects) => {
+    if (e.dashboard_title) return e.dashboard_title;
+    if (e.label) return e.label;
+    if (e.url && !e.table) return e.item_title;
+    if (e.item_title) return e.item_title;
+    return e.slice_name;
+  };
+
+  const getIconName = (e: ActivityObjects) => {
+    if (e.sql) return 'sql';
+    if (e.url?.includes('dashboard')) {
+      return 'nav-dashboard';
+    }
+    if (e.url?.includes('explore') || e.item_url?.includes('explore')) {
+      return 'nav-charts';
+    }
+    return '';
+  };
+
+  const tabs = [
+    {
+      name: 'Edited',
+      label: t('Edited'),
+      onClick: () => {
+        setActiveChild('Edited');
+      },
+    },
+    {
+      name: 'Created',
+      label: t('Created'),
+      onClick: () => {
+        setActiveChild('Created');
+      },
+    },
+  ];
+
+  if (activityData.Viewed) {
+    tabs.unshift({
+      name: 'Viewed',
+      label: t('Viewed'),
+      onClick: () => {
+        setActiveChild('Viewed');
+      },
+    });
+  } else {
+    tabs.unshift({
+      name: 'Examples',
+      label: t('Examples'),
+      onClick: () => {
+        setActiveChild('Examples');
+      },
+    });
+  }
+
+  useEffect(() => {
+    getRecentAcitivtyObjs(user.userId, recent, addDangerToast)
+      .then(res => {
+        const data: any = {
+          Created: [
+            ...res.createdByChart,
+            ...res.createdByDash,
+            ...res.createdByQuery,
+          ],
+          Edited: [...res.editedChart, ...res.editedDash],
+        };
+        if (res.viewed) {
+          const filtered = reject(res.viewed, ['item_url', null]).map(r => r);
+          data.Viewed = filtered;
+          setActiveChild('Viewed');
+        } else {
+          data.Examples = res.examples;
+          setActiveChild('Examples');
+        }
+        setActivityData(data);
+        setLoading(false);
+      })
+      .catch(e => {
+        setLoading(false);
+        addDangerToast(
+          `There was an issue fetching your recent Acitivity: ${e}`,
+        );
+      });
+  }, []);
+
+  const renderActivity = () => {
+    return activityData[activeChild].map((e: ActivityObjects) => (
+      <ListViewCard
+        key={`${e.id}`}
+        isRecent
+        loading={loading}
+        url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url}
+        title={getFilterTitle(e)}
+        description={`Last Edited: ${moment(e.changed_on_utc).format(
+          'MM/DD/YYYY HH:mm:ss',
+        )}`}
+        avatar={getIconName(e)}
+        actions={null}
+      />
+    ));
+  };
+  if (loading) return <>loading ...</>;
+  return (
+    <>
+      <SubMenu
+        activeChild={activeChild}
+        // eslint-disable-next-line react/no-children-prop
+        tabs={tabs}
+      />
+      <>
+        {activityData[activeChild]?.length > 0 ? (
+          <ActivityContainer>{renderActivity()}</ActivityContainer>
+        ) : (
+          <EmptyState tableName="RECENTS" tab={activeChild} />
+        )}
+      </>
+    </>
+  );
+}
diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
new file mode 100644
index 0000000..9a12dfd
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
@@ -0,0 +1,167 @@
+/**
+ * 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, { useEffect, useState } from 'react';
+import { t } from '@superset-ui/core';
+import { useListViewResource, useChartEditModal } from 'src/views/CRUD/hooks';
+import withToasts from 'src/messageToasts/enhancers/withToasts';
+import PropertiesModal from 'src/explore/components/PropertiesModal';
+import { User } from 'src/types/bootstrapTypes';
+import Icon from 'src/components/Icon';
+import ChartCard from 'src/views/CRUD/chart/ChartCard';
+import Chart from 'src/types/Chart';
+import SubMenu from 'src/components/Menu/SubMenu';
+import EmptyState from './EmptyState';
+import { CardContainer, IconContainer } from '../utils';
+
+const PAGE_SIZE = 3;
+
+interface ChartTableProps {
+  addDangerToast: (message: string) => void;
+  addSuccessToast: (message: string) => void;
+  search: string;
+  chartFilter?: string;
+  user?: User;
+}
+
+function ChartTable({
+  user,
+  addDangerToast,
+  addSuccessToast,
+}: ChartTableProps) {
+  const {
+    state: { loading, resourceCollection: charts, bulkSelectEnabled },
+    setResourceCollection: setCharts,
+    hasPerm,
+    refreshData,
+    fetchData,
+  } = useListViewResource<Chart>('chart', t('chart'), addDangerToast);
+  const {
+    sliceCurrentlyEditing,
+    openChartEditModal,
+    handleChartUpdated,
+    closeChartEditModal,
+  } = useChartEditModal(setCharts, charts);
+
+  const [chartFilter, setChartFilter] = useState('Mine');
+
+  const getFilters = () => {
+    const filters = [];
+
+    if (chartFilter === 'Mine') {
+      filters.push({
+        id: 'created_by',
+        operator: 'rel_o_m',
+        value: `${user?.userId}`,
+      });
+    } else {
+      filters.push({
+        id: 'id',
+        operator: 'chart_is_fav',
+        value: true,
+      });
+    }
+    return filters;
+  };
+
+  useEffect(() => {
+    fetchData({
+      pageIndex: 0,
+      pageSize: PAGE_SIZE,
+      sortBy: [
+        {
+          id: 'changed_on_delta_humanized',
+          desc: true,
+        },
+      ],
+      filters: getFilters(),
+    });
+  }, [chartFilter]);
+
+  return (
+    <>
+      {sliceCurrentlyEditing && (
+        <PropertiesModal
+          onHide={closeChartEditModal}
+          onSave={handleChartUpdated}
+          show
+          slice={sliceCurrentlyEditing}
+        />
+      )}
+
+      <SubMenu
+        activeChild={chartFilter}
+        // eslint-disable-next-line react/no-children-prop
+        tabs={[
+          {
+            name: 'Favorite',
+            label: t('Favorite'),
+            onClick: () => setChartFilter('Favorite'),
+          },
+          {
+            name: 'Mine',
+            label: t('Mine'),
+            onClick: () => setChartFilter('Mine'),
+          },
+        ]}
+        buttons={[
+          {
+            name: (
+              <IconContainer>
+                <Icon name="plus-small" />
+                {t('Chart')}
+              </IconContainer>
+            ),
+            buttonStyle: 'tertiary',
+            onClick: () => {
+              window.location.href = '/chart/add';
+            },
+          },
+          {
+            name: 'View All »',
+            buttonStyle: 'link',
+            onClick: () => {
+              window.location.href = '/chart/list';
+            },
+          },
+        ]}
+      />
+      {charts?.length ? (
+        <CardContainer>
+          {charts.map(e => (
+            <ChartCard
+              key={`${e.id}`}
+              openChartEditModal={openChartEditModal}
+              loading={loading}
+              chart={e}
+              hasPerm={hasPerm}
+              bulkSelectEnabled={bulkSelectEnabled}
+              refreshData={refreshData}
+              addDangerToast={addDangerToast}
+              addSuccessToast={addSuccessToast}
+            />
+          ))}
+        </CardContainer>
+      ) : (
+        <EmptyState tableName="CHARTS" tab={chartFilter} />
+      )}
+    </>
+  );
+}
+
+export default withToasts(ChartTable);
diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
index b3990f4..67536cd 100644
--- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
@@ -16,169 +16,176 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
-import { t, SupersetClient } from '@superset-ui/core';
-import { debounce } from 'lodash';
-import ListView, { FetchDataConfig } from 'src/components/ListView';
+import React, { useEffect, useState } from 'react';
+import { SupersetClient, t } from '@superset-ui/core';
+import { useListViewResource } from 'src/views/CRUD/hooks';
+import { Dashboard, DashboardTableProps } from 'src/views/CRUD/types';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
-import { Dashboard } from 'src/types/bootstrapTypes';
+import PropertiesModal from 'src/dashboard/components/PropertiesModal';
+import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard';
+import SubMenu from 'src/components/Menu/SubMenu';
+import Icon from 'src/components/Icon';
+import EmptyState from './EmptyState';
+import { createErrorHandler, CardContainer, IconContainer } from '../utils';
 
-const PAGE_SIZE = 25;
+const PAGE_SIZE = 3;
 
-interface DashboardTableProps {
-  addDangerToast: (message: string) => void;
-  search?: string;
+export interface FilterValue {
+  col: string;
+  operator: string;
+  value: string | boolean | number | null | undefined;
 }
 
-interface DashboardTableState {
-  dashboards: Dashboard[];
-  dashboard_count: number;
-  loading: boolean;
-}
+function DashboardTable({
+  user,
+  addDangerToast,
+  addSuccessToast,
+}: DashboardTableProps) {
+  const {
+    state: { loading, resourceCollection: dashboards, bulkSelectEnabled },
+    setResourceCollection: setDashboards,
+    hasPerm,
+    refreshData,
+    fetchData,
+  } = useListViewResource<Dashboard>(
+    'dashboard',
+    t('dashboard'),
+    addDangerToast,
+  );
 
-class DashboardTable extends React.PureComponent<
-  DashboardTableProps,
-  DashboardTableState
-> {
-  columns = [
-    {
-      accessor: 'dashboard_title',
-      Header: 'Dashboard',
-      Cell: ({
-        row: {
-          original: { url, dashboard_title: dashboardTitle },
-        },
-      }: {
-        row: {
-          original: {
-            url: string;
-            dashboard_title: string;
-          };
-        };
-      }) => <a href={url}>{dashboardTitle}</a>,
-    },
-    {
-      accessor: 'changed_by.first_name',
-      Header: 'Modified By',
-      Cell: ({
-        row: {
-          original: { changed_by_name: changedByName, changedByUrl },
-        },
-      }: {
-        row: {
-          original: {
-            changed_by_name: string;
-            changedByUrl: string;
-          };
-        };
-      }) => <a href={changedByUrl}>{changedByName}</a>,
-    },
-    {
-      accessor: 'changed_on_delta_humanized',
-      Header: 'Modified',
-      Cell: ({
-        row: {
-          original: { changed_on_delta_humanized: changedOn },
-        },
-      }: {
-        row: {
-          original: {
-            changed_on_delta_humanized: string;
-          };
-        };
-      }) => <span className="no-wrap">{changedOn}</span>,
-    },
-  ];
+  const [editModal, setEditModal] = useState<Dashboard>();
+  const [dashboardFilter, setDashboardFilter] = useState('Mine');
 
-  initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
+  const handleDashboardEdit = (edits: Dashboard) => {
+    return SupersetClient.get({
+      endpoint: `/api/v1/dashboard/${edits.id}`,
+    }).then(
+      ({ json = {} }) => {
+        setDashboards(
+          dashboards.map(dashboard => {
+            if (dashboard.id === json.id) {
+              return json.result;
+            }
+            return dashboard;
+          }),
+        );
+      },
+      createErrorHandler(errMsg =>
+        addDangerToast(
+          t('An error occurred while fetching dashboards: %s', errMsg),
+        ),
+      ),
+    );
+  };
 
-  constructor(props: DashboardTableProps) {
-    super(props);
-    this.state = {
-      dashboards: [],
-      dashboard_count: 0,
-      loading: false,
-    };
-  }
+  const getFilters = () => {
+    const filters = [];
 
-  componentDidUpdate(prevProps: DashboardTableProps) {
-    if (prevProps.search !== this.props.search) {
-      this.fetchDataDebounced({
-        pageSize: PAGE_SIZE,
-        pageIndex: 0,
-        sortBy: this.initialSort,
-        filters: [],
+    if (dashboardFilter === 'Mine') {
+      filters.push({
+        id: 'owners',
+        operator: 'rel_m_m',
+        value: `${user?.userId}`,
+      });
+    } else {
+      filters.push({
+        id: 'id',
+        operator: 'dashboard_is_fav',
+        value: true,
       });
     }
+    return filters;
+  };
+  const subMenus = [];
+  if (dashboards.length > 0 && dashboardFilter === 'favorite') {
+    subMenus.push({
+      name: 'Favorite',
+      label: t('Favorite'),
+      onClick: () => setDashboardFilter('Favorite'),
+    });
   }
 
-  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,
-      }))
-      .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 } : {}),
+  useEffect(() => {
+    fetchData({
+      pageIndex: 0,
+      pageSize: PAGE_SIZE,
+      sortBy: [
+        {
+          id: 'changed_on_delta_humanized',
+          desc: true,
+        },
+      ],
+      filters: getFilters(),
     });
+  }, [dashboardFilter]);
 
-    return SupersetClient.get({
-      endpoint: `/api/v1/dashboard/?q=${queryParams}`,
-    })
-      .then(({ json }) => {
-        this.setState({ dashboards: json.result, dashboard_count: json.count });
-      })
-      .catch(response => {
-        if (response.status === 401) {
-          this.props.addDangerToast(
-            t(
-              "You don't have the necessary permissions to load dashboards. Please contact your administrator.",
+  return (
+    <>
+      <SubMenu
+        activeChild={dashboardFilter}
+        tabs={[
+          {
+            name: 'Favorite',
+            label: t('Favorite'),
+            onClick: () => setDashboardFilter('Favorite'),
+          },
+          {
+            name: 'Mine',
+            label: t('Mine'),
+            onClick: () => setDashboardFilter('Mine'),
+          },
+        ]}
+        buttons={[
+          {
+            name: (
+              <IconContainer>
+                <Icon name="plus-small" /> Dashboard{' '}
+              </IconContainer>
             ),
-          );
-        } else {
-          this.props.addDangerToast(
-            t('An error occurred while fetching Dashboards'),
-          );
-        }
-      })
-      .finally(() => this.setState({ loading: false }));
-  };
-
-  // sort-comp disabled because of conflict with no-use-before-define rule
-  // eslint-disable-next-line react/sort-comp
-  fetchDataDebounced = debounce(this.fetchData, 200);
-
-  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}
+            buttonStyle: 'tertiary',
+            onClick: () => {
+              window.location.href = '/dashboard/new';
+            },
+          },
+          {
+            name: 'View All »',
+            buttonStyle: 'link',
+            onClick: () => {
+              window.location.href = '/dashboard/list/';
+            },
+          },
+        ]}
       />
-    );
-  }
+      {editModal && (
+        <PropertiesModal
+          dashboardId={editModal?.id}
+          show
+          onHide={() => setEditModal(undefined)}
+          onSubmit={handleDashboardEdit}
+        />
+      )}
+      {dashboards.length > 0 ? (
+        <CardContainer>
+          {dashboards.map(e => (
+            <DashboardCard
+              {...{
+                dashboard: e,
+                hasPerm,
+                bulkSelectEnabled,
+                refreshData,
+                addDangerToast,
+                addSuccessToast,
+                loading,
+                openDashboardEditModal: dashboard => setEditModal(dashboard),
+              }}
+            />
+          ))}
+        </CardContainer>
+      ) : (
+        <EmptyState tableName="DASHBOARDS" tab={dashboardFilter} />
+      )}
+    </>
+  );
 }
 
 export default withToasts(DashboardTable);
diff --git a/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
new file mode 100644
index 0000000..f145dfe
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
@@ -0,0 +1,144 @@
+/**
+ * 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 Button from 'src/components/Button';
+import { Empty } from 'src/common/components';
+import { t, styled } from '@superset-ui/core';
+import Icon from 'src/components/Icon';
+import { IconContainer } from '../utils';
+
+interface EmptyStateProps {
+  tableName: string;
+  tab?: string;
+}
+
+const ButtonContainer = styled.div`
+  Button {
+    svg {
+      color: ${({ theme }) => theme.colors.grayscale.light5};
+    }
+  }
+`;
+
+export default function EmptyState({ tableName, tab }: EmptyStateProps) {
+  const mineRedirects = {
+    DASHBOARDS: '/dashboard/new',
+    CHARTS: '/chart/add',
+    SAVED_QUERIES: '/superset/sqllab',
+  };
+  const favRedirects = {
+    DASHBOARDS: '/dashboard/list/',
+    CHARTS: '/chart/list',
+    SAVED_QUERIES: '/savedqueryview/list/',
+  };
+  const tableIcon = {
+    RECENTS: 'union.png',
+    DASHBOARDS: 'empty-dashboard.png',
+    CHARTS: 'empty-charts.png',
+    SAVED_QUERIES: 'empty-queries.png',
+  };
+  const mine = (
+    <div>{`No ${
+      tableName === 'SAVED_QUERIES'
+        ? t('saved queries')
+        : t(`${tableName.toLowerCase()}`)
+    } yet`}</div>
+  );
+  const recent = (
+    <div className="no-recents">
+      {(() => {
+        if (tab === 'Viewed') {
+          return t(
+            `Recently viewed charts, dashboards, and saved queries will appear here`,
+          );
+        }
+        if (tab === 'Created') {
+          return t(
+            'Recently created charts, dashboards, and saved queries will appear here',
+          );
+        }
+        if (tab === 'Examples') {
+          return t(
+            `Recent example charts, dashboards, and saved queries will appear here`,
+          );
+        }
+        if (tab === 'Edited') {
+          return t(
+            `Recently edited charts, dashboards, and saved queries will appear here`,
+          );
+        }
+        return null;
+      })()}
+    </div>
+  );
+  // Mine and Recent Activity(all tabs) tab empty state
+  if (tab === 'Mine' || tableName === 'RECENTS') {
+    return (
+      <Empty
+        image={`/static/assets/images/${tableIcon[tableName]}`}
+        description={tableName === 'RECENTS' ? recent : mine}
+      >
+        {tableName !== 'RECENTS' && (
+          <ButtonContainer>
+            <Button
+              buttonStyle="primary"
+              onClick={() => {
+                window.location = mineRedirects[tableName];
+              }}
+            >
+              <IconContainer>
+                <Icon name="plus-small" />{' '}
+                {tableName === 'SAVED_QUERIES'
+                  ? t('SQL QUERY')
+                  : t(`${tableName
+                      .split('')
+                      .slice(0, tableName.length - 1)
+                      .join('')}
+                    `)}
+              </IconContainer>
+            </Button>
+          </ButtonContainer>
+        )}
+      </Empty>
+    );
+  }
+  // Favorite tab empty state
+  return (
+    <Empty
+      image="/static/assets/images/star-circle.png"
+      description={
+        <div className="no-favorites">
+          {t("You don't have any favorites yet!")}
+        </div>
+      }
+    >
+      <Button
+        buttonStyle="primary"
+        onClick={() => {
+          window.location = favRedirects[tableName];
+        }}
+      >
+        SEE ALL{' '}
+        {tableName === 'SAVED_QUERIES'
+          ? t('SQL LAB QUERIES')
+          : t(`${tableName}`)}
+      </Button>
+    </Empty>
+  );
+}
diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
new file mode 100644
index 0000000..dc0590b
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
@@ -0,0 +1,260 @@
+/**
+ * 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, { useEffect, useState } from 'react';
+import { t, SupersetClient, styled } from '@superset-ui/core';
+import withToasts from 'src/messageToasts/enhancers/withToasts';
+import { Dropdown, Menu } from 'src/common/components';
+import { useListViewResource, copyQueryLink } from 'src/views/CRUD/hooks';
+import ListViewCard from 'src/components/ListViewCard';
+import DeleteModal from 'src/components/DeleteModal';
+import Icon from 'src/components/Icon';
+import SubMenu from 'src/components/Menu/SubMenu';
+import EmptyState from './EmptyState';
+
+import { IconContainer, CardContainer, createErrorHandler } from '../utils';
+
+const PAGE_SIZE = 3;
+
+interface Query {
+  id?: number;
+  sql_tables?: Array<any>;
+  database?: {
+    database_name: string;
+  };
+  rows?: string;
+  description?: string;
+  end_time?: string;
+  label?: string;
+}
+
+interface SavedQueriesProps {
+  user: {
+    userId: string | number;
+  };
+  queryFilter: string;
+  addDangerToast: (arg0: string) => void;
+  addSuccessToast: (arg0: string) => void;
+}
+
+const QueryData = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+  .title {
+    font-weight: ${({ theme }) => theme.typography.weights.normal};
+    color: ${({ theme }) => theme.colors.grayscale.light2};
+  }
+  .holder {
+    margin: ${({ theme }) => theme.gridUnit * 2}px;
+  }
+`;
+const SavedQueries = ({
+  user,
+  addDangerToast,
+  addSuccessToast,
+}: SavedQueriesProps) => {
+  const {
+    state: { loading, resourceCollection: queries },
+    hasPerm,
+    fetchData,
+    refreshData,
+  } = useListViewResource<Query>('saved_query', t('query'), addDangerToast);
+  const [queryFilter, setQueryFilter] = useState('Mine');
+  const [queryDeleteModal, setQueryDeleteModal] = useState(false);
+  const [currentlyEdited, setCurrentlyEdited] = useState<Query>({});
+
+  const canEdit = hasPerm('can_edit');
+  const canDelete = hasPerm('can_delete');
+
+  const handleQueryDelete = ({ id, label }: Query) => {
+    SupersetClient.delete({
+      endpoint: `/api/v1/saved_query/${id}`,
+    }).then(
+      () => {
+        refreshData();
+        setQueryDeleteModal(false);
+        addSuccessToast(t('Deleted: %s', label));
+      },
+      createErrorHandler(errMsg =>
+        addDangerToast(t('There was an issue deleting %s: %s', label, errMsg)),
+      ),
+    );
+  };
+
+  const getFilters = () => {
+    const filters = [];
+    if (queryFilter === 'Mine') {
+      filters.push({
+        id: 'created_by',
+        operator: 'rel_o_m',
+        value: `${user?.userId}`,
+      });
+    } else {
+      filters.push({
+        id: 'id',
+        operator: 'saved_query_is_fav',
+        value: true,
+      });
+    }
+    return filters;
+  };
+
+  useEffect(() => {
+    fetchData({
+      pageIndex: 0,
+      pageSize: PAGE_SIZE,
+      sortBy: [
+        {
+          id: 'changed_on_delta_humanized',
+          desc: true,
+        },
+      ],
+      filters: getFilters(),
+    });
+  }, [queryFilter]);
+
+  const renderMenu = (query: Query) => (
+    <Menu>
+      {canEdit && (
+        <Menu.Item
+          onClick={() => {
+            window.location.href = `/superset/sqllab?savedQueryId=${query.id}`;
+          }}
+        >
+          {t('Edit')}
+        </Menu.Item>
+      )}
+      <Menu.Item
+        onClick={() => {
+          if (query.id)
+            copyQueryLink(query.id, addDangerToast, addSuccessToast);
+        }}
+      >
+        {t('Share')}
+      </Menu.Item>
+      {canDelete && (
+        <Menu.Item
+          onClick={() => {
+            setQueryDeleteModal(true);
+            setCurrentlyEdited(query);
+          }}
+        >
+          {t('Delete')}
+        </Menu.Item>
+      )}
+    </Menu>
+  );
+  return (
+    <>
+      {queryDeleteModal && (
+        <DeleteModal
+          description={t(
+            'This action will permanently delete the saved query.',
+          )}
+          onConfirm={() => {
+            if (queryDeleteModal) {
+              handleQueryDelete(currentlyEdited);
+            }
+          }}
+          onHide={() => {
+            setQueryDeleteModal(false);
+          }}
+          open
+          title={t('Delete Query?')}
+        />
+      )}
+      <SubMenu
+        activeChild={queryFilter}
+        tabs={[
+          {
+            name: 'Favorite',
+            label: t('Favorite'),
+            onClick: () => setQueryFilter('Favorite'),
+          },
+          {
+            name: 'Mine',
+            label: t('Mine'),
+            onClick: () => setQueryFilter('Mine'),
+          },
+        ]}
+        buttons={[
+          {
+            name: (
+              <IconContainer>
+                <Icon name="plus-small" /> SQL Query{' '}
+              </IconContainer>
+            ),
+            buttonStyle: 'tertiary',
+            onClick: () => {
+              window.location.href = '/superset/sqllab';
+            },
+          },
+          {
+            name: 'View All »',
+            buttonStyle: 'link',
+            onClick: () => {
+              window.location.href = '/savedqueryview/list';
+            },
+          },
+        ]}
+      />
+      {queries.length > 0 ? (
+        <CardContainer>
+          {queries.map(q => (
+            <ListViewCard
+              key={`${q.id}`}
+              imgFallbackURL=""
+              imgURL=""
+              url={`/superset/sqllab?savedQueryId=${q.id}`}
+              title={q.label}
+              rows={q.rows}
+              loading={loading}
+              description={t('Last run ', q.end_time)}
+              showImg={false}
+              renderCover={
+                <QueryData>
+                  <div className="holder">
+                    <div className="title">{t('Tables')}</div>
+                    <div>{q?.sql_tables?.length}</div>
+                  </div>
+                  <div className="holder">
+                    <div className="title">{t('Datasource Name')}</div>
+                    <div>{q?.sql_tables && q.sql_tables[0]?.table}</div>
+                  </div>
+                </QueryData>
+              }
+              actions={
+                <ListViewCard.Actions>
+                  <Dropdown overlay={renderMenu(q)}>
+                    <Icon name="more-horiz" />
+                  </Dropdown>
+                </ListViewCard.Actions>
+              }
+            />
+          ))}
+        </CardContainer>
+      ) : (
+        <EmptyState tableName="SAVED_QUERIES" tab={queryFilter} />
+      )}
+    </>
+  );
+};
+
+export default withToasts(SavedQueries);
diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
index 256fe3a..f35effc 100644
--- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
@@ -16,128 +16,79 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useCallback, useState } from 'react';
-import {
-  Panel,
-  Row,
-  Col,
-  Tabs,
-  Tab,
-  FormControl,
-  FormControlProps,
-} from 'react-bootstrap';
-import { t } from '@superset-ui/core';
-import { useQueryParam, StringParam, QueryParamConfig } from 'use-query-params';
+import React from 'react';
+import { styled, t } from '@superset-ui/core';
+import { Collapse } from 'src/common/components';
 import { User } from 'src/types/bootstrapTypes';
-import RecentActivity from 'src/profile/components/RecentActivity';
-import Favorites from 'src/profile/components/Favorites';
+import { mq } from '../utils';
+import ActivityTable from './ActivityTable';
+import ChartTable from './ChartTable';
+import SavedQueries from './SavedQueries';
 import DashboardTable from './DashboardTable';
 
+const { Panel } = Collapse;
+
 interface WelcomeProps {
   user: User;
 }
 
-function useSyncQueryState(
-  queryParam: string,
-  queryParamType: QueryParamConfig<
-    string | null | undefined,
-    string | undefined
-  >,
-  defaultState: string,
-): [string, (val: string) => void] {
-  const [queryState, setQueryState] = useQueryParam(queryParam, queryParamType);
-  const [state, setState] = useState(queryState || defaultState);
-
-  const setQueryStateAndState = (val: string) => {
-    setQueryState(val);
-    setState(val);
-  };
-
-  return [state, setQueryStateAndState];
-}
+const WelcomeContainer = styled.div`
+  background-color: ${({ theme }) => theme.colors.grayscale.light4};
+  nav {
+    margin-top: -15px;
+    background-color: ${({ theme }) => theme.colors.grayscale.light4};
+    &:after {
+      content: '';
+      display: block;
+      border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+      margin: 0px ${({ theme }) => theme.gridUnit * 6}px;
+      position: relative;
+      ${[mq[1]]} {
+        margin-top: 5px;
+        margin: 0px 2px;
+      }
+    }
+    .nav.navbar-nav {
+      & > li:nth-child(1),
+      & > li:nth-child(2),
+      & > li:nth-child(3) {
+        margin-top: ${({ theme }) => theme.gridUnit * 2}px;
+      }
+    }
+    button {
+      padding: 3px 21px;
+    }
+    .navbar-right {
+      position: relative;
+      top: 11px;
+    }
+  }
+  .ant-card.ant-card-bordered {
+    border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+  }
+  .ant-collapse-header {
+    font-weight: ${({ theme }) => theme.typography.weights.normal};
+    font-size: ${({ theme }) => theme.gridUnit * 4}px;
+  }
+`;
 
 export default function Welcome({ user }: WelcomeProps) {
-  const [activeTab, setActiveTab] = useSyncQueryState(
-    'activeTab',
-    StringParam,
-    'all',
-  );
-
-  const [searchQuery, setSearchQuery] = useSyncQueryState(
-    'search',
-    StringParam,
-    '',
-  );
-
-  const onFormControlChange = useCallback(
-    (e: React.FormEvent<FormControl & FormControlProps>) => {
-      const { value } = e.currentTarget;
-      setSearchQuery((value as string) ?? '');
-    },
-    [],
-  );
-
-  const onTabsSelect = useCallback((e: any) => {
-    setActiveTab(e as string);
-  }, []);
-
   return (
-    <div className="container welcome">
-      <Tabs
-        activeKey={activeTab}
-        onSelect={onTabsSelect}
-        id="uncontrolled-tab-example"
-      >
-        <Tab eventKey="all" title={t('Dashboards')}>
-          <Panel>
-            <Panel.Body>
-              <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={onFormControlChange}
-                  />
-                </Col>
-              </Row>
-              <hr />
-              <DashboardTable search={searchQuery} />
-            </Panel.Body>
-          </Panel>
-        </Tab>
-        <Tab eventKey="recent" title={t('Recently Viewed')}>
-          <Panel>
-            <Panel.Body>
-              <Row>
-                <Col md={8}>
-                  <h2>{t('Recently Viewed')}</h2>
-                </Col>
-              </Row>
-              <hr />
-              <RecentActivity user={user} />
-            </Panel.Body>
-          </Panel>
-        </Tab>
-        <Tab eventKey="favorites" title={t('Favorites')}>
-          <Panel>
-            <Panel.Body>
-              <Row>
-                <Col md={8}>
-                  <h2>{t('Favorites')}</h2>
-                </Col>
-              </Row>
-              <hr />
-              <Favorites user={user} />
-            </Panel.Body>
-          </Panel>
-        </Tab>
-      </Tabs>
-    </div>
+    <WelcomeContainer>
+      <Collapse defaultActiveKey={['1', '2', '3', '4']} ghost>
+        <Panel header={t('Recents')} key="1">
+          <ActivityTable user={user} />
+        </Panel>
+        <Panel header={t('Dashboards')} key="2">
+          <DashboardTable user={user} />
+        </Panel>
+        <Panel header={t('Saved Queries')} key="3">
+          <SavedQueries user={user} />
+        </Panel>
+        <Panel header={t('Charts')} key="4">
+          <ChartTable user={user} />
+        </Panel>
+      </Collapse>
+    </WelcomeContainer>
   );
 }
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 3ef27d0..6ebe3cb 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -144,6 +144,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
     ]
     search_columns = [
         "created_by",
+        "changed_by",
         "datasource_id",
         "datasource_name",
         "datasource_type",
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index 8f65f35..cba494a 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -157,6 +157,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         "owners",
         "published",
         "slug",
+        "changed_by",
     )
     search_filters = {
         "dashboard_title": [DashboardTitleOrSlugFilter],
diff --git a/superset/queries/api.py b/superset/queries/api.py
index 0f368d3..ce4fca5 100644
--- a/superset/queries/api.py
+++ b/superset/queries/api.py
@@ -42,6 +42,9 @@ class QueryRestApi(BaseSupersetModelRestApi):
         "status",
         "start_time",
         "end_time",
+        "rows",
+        "tmp_table_name",
+        "tracking_url",
     ]
     show_columns = [
         "client_id",
diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py
index 794ab25..b37fc7d 100644
--- a/superset/queries/saved_queries/api.py
+++ b/superset/queries/saved_queries/api.py
@@ -106,7 +106,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
         "last_run_delta_humanized",
     ]
 
-    search_columns = ["id", "database", "label", "schema"]
+    search_columns = ["id", "database", "label", "schema", "created_by"]
     search_filters = {
         "id": [SavedQueryFavoriteFilter],
         "label": [SavedQueryAllTextFilter],