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

yjc pushed a commit to branch home-screen-mvp
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git

commit 1b46423c112742728919bb91e1430147e7384ee8
Author: Phillip Kelley-Dotson <[email protected]>
AuthorDate: Mon Oct 19 07:28:25 2020 -0700

    update branch
---
 superset-frontend/images/star-circle.png           | Bin 0 -> 2705 bytes
 superset-frontend/images/union.png                 | Bin 0 -> 1131 bytes
 .../views/CRUD/chart/ChartList_spec.jsx            |   1 +
 .../views/CRUD/welcome/ActivityTable_spec.tsx      |  93 +++++++++
 .../views/CRUD/welcome/ChartTable_spec.tsx         |   1 -
 .../views/CRUD/welcome/DashboardTable_spec.tsx     |  78 +++++---
 .../views/CRUD/welcome/SavedQueries_spec.tsx       |  30 ++-
 .../views/CRUD/welcome/Welcome_spec.tsx            |  14 +-
 .../src/components/ListViewCard/index.tsx          |  31 ++-
 .../src/views/CRUD/chart/ChartCard.tsx             |  13 +-
 .../src/views/CRUD/dashboard/DashboardCard.tsx     |  13 +-
 .../views/CRUD/data/savedquery/SavedQueryList.tsx  |  15 +-
 superset-frontend/src/views/CRUD/hooks.ts          |  49 ++++-
 superset-frontend/src/views/CRUD/types.ts          |  15 +-
 superset-frontend/src/views/CRUD/utils.tsx         | 130 +++++++++++--
 .../src/views/CRUD/welcome/ActivityTable.tsx       | 181 +++++++++++------
 .../src/views/CRUD/welcome/ChartTable.tsx          |  81 ++++++--
 .../src/views/CRUD/welcome/DashboardTable.tsx      | 148 ++++++++++----
 .../src/views/CRUD/welcome/EmptyState.tsx          | 112 +++++++++++
 .../src/views/CRUD/welcome/SavedQueries.tsx        | 177 ++++++++++++++---
 .../src/views/CRUD/welcome/Welcome.tsx             | 215 +++------------------
 21 files changed, 974 insertions(+), 423 deletions(-)

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..ca1dbe5
Binary files /dev/null and b/superset-frontend/images/union.png differ
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..2f54d58
--- /dev/null
+++ 
b/superset-frontend/spec/javascripts/views/CRUD/welcome/ActivityTable_spec.tsx
@@ -0,0 +1,93 @@
+/**
+ * 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';
+import ListViewCard from 'src/components/ListViewCard';
+
+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('renders', () => {
+    expect(wrapper.find(ActivityTable)).toExist();
+  });
+
+  it('renders a EmptyState', () => {
+    console.log('wrapper', wrapper.debug())
+  });
+
+  it('calls batch method and renders ListViewCArd', () => {
+    const chartCall = fetchMock.calls(/chart\/\?q/);
+    const dashboardCall = fetchMock.calls(/dashboard\/\?q/);
+    expect(chartCall).toHaveLength(2);
+    expect(dashboardCall).toHaveLength(2);
+    expect(wrapper.find(ListViewCard)).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
index ed0666e..a631a78 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/ChartTable_spec.tsx
@@ -21,7 +21,6 @@ import { mount } from 'enzyme';
 import thunk from 'redux-thunk';
 import fetchMock from 'fetch-mock';
 
-import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
 import configureStore from 'redux-mock-store';
 import ChartTable from 'src/views/CRUD/welcome/ChartTable';
 import ChartCard from 'src/views/CRUD/chart/ChartCard';
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 4d2eb9e..dd1e92a 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 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/welcome/DashboardCard';
+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 chartsInfoEndpoint = 'glob:*/api/v1/chart/_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(dashboardsEndpointNoData, { result: [] });
+fetchMock.get(chartsInfoEndpoint, {
+  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 },
   });
-}
+  // console.log('wrapper', wrapper.debug())
+  // beforeEach(fetchMock.resetHistory);
 
-describe('DashboardTable', () => {
-  beforeEach(fetchMock.resetHistory);
+  beforeAll(async () => {
+    await waitForComponentToPaint(wrapper);
+  });
 
-  it('fetches dashboards and renders a ', () => {
-    return new Promise(done => {
-      const wrapper = setup();
+  it('renders', () => {
+    expect(wrapper.find(DashboardTable)).toExist();
+    console.log('wrapper', wrapper.debug())
+  });
 
-      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(DashboardCard)).toExist();
-        done();
-      });
+  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(2);
+    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();
   });
+  /*it('display EmptyState if there is no data', ()=>{
+    (const wrapper = mount(<DashboardTable {...dashboardProps} />)
+    console.log('wrapper', wrapper);
+  });*/
 });
diff --git 
a/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx 
b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx
index d5c61b5..fd367a1 100644
--- 
a/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx
+++ 
b/superset-frontend/spec/javascripts/views/CRUD/welcome/SavedQueries_spec.tsx
@@ -17,16 +17,30 @@
  * under the License.
  */
 import React from 'react';
-import { SavedQueries } from 'src/views/CRUD/welcome/SavedQueries'
-import { shallow } from 'enzyme';
+import SavedQueries from 'src/views/CRUD/welcome/SavedQueries';
+import { mount } from 'enzyme';
+import thunk from 'redux-thunk';
+
+import configureStore from 'redux-mock-store';
+// store needed for withToasts(DashboardTable)
+const mockStore = configureStore([thunk]);
+const store = mockStore({});
 
 describe('SavedQueries', () => {
-  const 
+  const savedQueryProps = {
+    user: {
+      userId: '1',
+    },
+    activityFilter: 'Edit',
+  };
+
+  const wrapper = mount(<SavedQueries {...savedQueryProps} />);
+
   it('is valid', () => {
     expect(React.isValidElement(<SavedQueries />)).toBe(true);
   });
-  it('SaveQueries renders up to three saved queries', ()=>{
-    //expect() 
-  })
-  
-});
\ No newline at end of file
+ it('takes in props', () => {
+
+    // expect()
+  }); 
+});
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 13c27b7..38891c0 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/welcome/Welcome_spec.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import React from 'react';
-import { shallow } from 'enzyme';
+import { styledMount as mount } from 'spec/helpers/theming';
 
 import Welcome from 'src/views/CRUD/welcome/Welcome';
 
@@ -33,12 +33,12 @@ describe('Welcome', () => {
       isActive: true,
     },
   };
-  it('is valid', () => {
-    expect(React.isValidElement(<Welcome {...mockedProps} />)).toBe(true);
+  const wrapper = mount(<Welcome {...mockedProps} />);
+  it('is renders', () => {
+    expect(wrapper.find(Welcome)).toExist();
   });
-  it('renders 3 submenu components', () => {
-    const wrapper = shallow(<Welcome {...mockedProps} />);
-    expect(wrapper.find('SubMenu')).toHaveLength(4);
+  it('renders first submenu on page load', () => {
+    expect(wrapper.find('SubMenu')).toHaveLength(1);
+    expect(wrapper.find('PanelContent')).toHaveLength(4);
   });
-  console.log('wrapper', )
 });
diff --git a/superset-frontend/src/components/ListViewCard/index.tsx 
b/superset-frontend/src/components/ListViewCard/index.tsx
index 427beba..674985b 100644
--- a/superset-frontend/src/components/ListViewCard/index.tsx
+++ b/superset-frontend/src/components/ListViewCard/index.tsx
@@ -139,11 +139,26 @@ const SkeletonActions = styled(Skeleton.Button)`
   width: ${({ theme }) => theme.gridUnit * 10}px;
 `;
 
+const QueryData = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  border-bottom: 1px solid #e0e0e0;
+  .title {
+    font-weight: 500;
+    color: #b2b2b2;
+  }
+  .holder {
+    margin: 10px 10px 10px 10px;
+  }
+`;
+
 const paragraphConfig = { rows: 1, width: 150 };
 interface CardProps {
   title: React.ReactNode;
   url?: string;
   imgURL: string;
+  tables?: string | number;
   imgFallbackURL: string;
   imgPosition?: BackgroundPosition;
   description: string;
@@ -167,6 +182,7 @@ function ListViewCard({
   imgFallbackURL,
   description,
   coverLeft,
+  tables,
   coverRight,
   actions,
   avatar,
@@ -174,7 +190,6 @@ function ListViewCard({
   loading,
   imgPosition = 'top',
   showImg = true,
-  rows,
   isRecent,
 }: CardProps) {
   return (
@@ -205,16 +220,12 @@ function ListViewCard({
           </Cover>
         ) : (
           <QueryData>
-            <div>
-              <div>Tables</div>
-              <div>{}</div>
-            </div>
-            <div>
-              <div>Rows</div>
-              <div>{rows}</div>
+            <div className="holder">
+              <div className="title">Tables</div>
+              <div>{tables}</div>
             </div>
-            <div>
-              <div>Datasource Name</div>
+            <div className="holder">
+              <div className="title">Datasource Name</div>
               <div>{tableName}</div>
             </div>
           </QueryData>
diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx 
b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
index e1ab61f..d5cd625 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx
@@ -54,11 +54,12 @@ export default function ChartCard({
 }: ChartCardProps) {
   const canEdit = hasPerm('can_edit');
   const canDelete = hasPerm('can_delete');
-  const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
-    {},
-    FAVESTAR_BASE_URL,
-    addDangerToast,
-  );
+  const [
+    favoriteStatusRef,
+    fetchFaveStar,
+    saveFaveStar,
+    favoriteStatus,
+  ] = useFavoriteStatus({}, FAVESTAR_BASE_URL, addDangerToast);
 
   function handleChartDelete({ id, slice_name: sliceName }: Chart) {
     SupersetClient.delete({
@@ -139,7 +140,7 @@ export default function ChartCard({
             itemId={chart.id}
             fetchFaveStar={fetchFaveStar}
             saveFaveStar={saveFaveStar}
-            isStarred={!!favoriteStatusRef.current[chart.id]}
+            isStarred={!!favoriteStatus[chart.id]}
             height={20}
             width={20}
           />
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx 
b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
index e2e3b1f..2a09f70 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx
@@ -27,11 +27,12 @@ function DashboardCard({
   const canEdit = hasPerm('can_edit');
   const canDelete = hasPerm('can_delete');
   const canExport = hasPerm('can_mulexport');
-  const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
-    {},
-    FAVESTAR_BASE_URL,
-    addDangerToast,
-  );
+  const [
+    favoriteStatusRef,
+    fetchFaveStar,
+    saveFaveStar,
+    favoriteStatus,
+  ] = useFavoriteStatus({}, FAVESTAR_BASE_URL, addDangerToast);
 
   function handleDashboardDelete({
     id,
@@ -126,7 +127,7 @@ function DashboardCard({
             itemId={dashboard.id}
             fetchFaveStar={fetchFaveStar}
             saveFaveStar={saveFaveStar}
-            isStarred={!!favoriteStatusRef.current[dashboard.id]}
+            isStarred={!!favoriteStatus[dashboard.id]}
           />
           <Dropdown overlay={menu}>
             <Icon name="more-horiz" />
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 80de0a7..9dbdbc2 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -330,6 +330,7 @@ export function useFavoriteStatus(
       endpoint: `${baseURL}/${id}/count/`,
     }).then(
       ({ json }) => {
+        console.log('json', json);
         updateFavoriteStatus({ [id]: json.count > 0 });
       },
       createErrorHandler(errMsg =>
@@ -357,7 +358,12 @@ export function useFavoriteStatus(
     );
   };
 
-  return [favoriteStatusRef, fetchFaveStar, saveFaveStar] as const;
+  return [
+    favoriteStatusRef,
+    fetchFaveStar,
+    saveFaveStar,
+    favoriteStatus,
+  ] as const;
 }
 
 export const useChartEditModal = (
@@ -397,3 +403,44 @@ export const useChartEditModal = (
     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();
+    }
+    console.log('-----Success Toast--------')
+    addSuccessToast(t('Link Copied!'));
+    console.log('-----Success Toast--------')
+  }
+};
diff --git a/superset-frontend/src/views/CRUD/types.ts 
b/superset-frontend/src/views/CRUD/types.ts
index afd1956..1070f57 100644
--- a/superset-frontend/src/views/CRUD/types.ts
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -27,7 +27,6 @@ export interface DashboardTableProps {
   addDangerToast: (message: string) => void;
   addSuccessToast: (message: string) => void;
   search: string;
-  dashboardFilter?: string;
   user?: User;
 }
 
@@ -56,3 +55,17 @@ export interface DashboardCardProps {
   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 }>;
+};
\ No newline at end of file
diff --git a/superset-frontend/src/views/CRUD/utils.tsx 
b/superset-frontend/src/views/CRUD/utils.tsx
index 6caabb9..2208cc6 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -20,6 +20,7 @@ import {
   SupersetClient,
   SupersetClientResponse,
   logging,
+  styled,
 } from '@superset-ui/core';
 import rison from 'rison';
 import getClientErrorObject from 'src/utils/getClientErrorObject';
@@ -53,27 +54,82 @@ const createFetchResourceMethod = (method: string) => (
   return [];
 };
 
-export const createBatchMethod = (queryParams: string, created?: string) => {
+export const getBatchData = (userId: string, recent: string) => {
+  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: `/api/v1/dashboard/?q=${queryParams}` }),
-    SupersetClient.get({ endpoint: `/api/v1/chart/?q=${queryParams}` }),
+    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)}`,
+    }),
   ];
-  if (created)
-    baseBatch.push(
-      SupersetClient.get({ endpoint: `/api/v1/saved_query/?q=${queryParams}` 
}),
-    );
-  return Promise.all(baseBatch).then(([dashboardRes, chartRes, savedQuery]) => 
{
-    const results = [];
-    const ifQuery = savedQuery ? savedQuery.json?.result.slice(0, 3) : [];
-    results.push(
-      ...[
-        ...dashboardRes.json?.result.slice(0, 3),
-        ...chartRes.json?.result.slice(0, 3),
-        ...ifQuery,
-      ],
-    );
-    return results;
-  });
+  return Promise.all(baseBatch).then(
+    // @ts-ignore
+    ([recentsRes, editedDash, editedChart, createdByDash, createdByChart]) => {
+      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),
+      };
+      if (recentsRes.json.length === 0) {
+        const newBatch = [
+          SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }),
+          SupersetClient.get({
+            endpoint: `/api/v1/dashboard/?q=${getParams()}`,
+          }),
+        ];
+        // @ts-ignore
+        return Promise.all(newBatch)
+          .then(([chartRes, dashboardRes]) => {
+            res.examples = [
+              ...chartRes.json.result,
+              ...dashboardRes.json.result,
+            ];
+            return res;
+          })
+          .catch(e => console.log('err', e));
+      }
+      res.viewed = recentsRes.json;
+      return res;
+    },
+  );
 };
 
 export const createFetchRelated = createFetchResourceMethod('related');
@@ -86,3 +142,39 @@ export function createErrorHandler(handleErrorFunc: 
(errMsg?: string) => void) {
     handleErrorFunc(parsedError.message || parsedError.error);
   };
 }
+
+export function handleDashboardDelete(id: string) {
+  return SupersetClient.delete({
+    endpoint: `/api/v1/dashboard/${id}`,
+  });
+}
+
+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(48%, 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
index 669838d..02d2cbc 100644
--- a/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ActivityTable.tsx
@@ -17,11 +17,15 @@
  * under the License.
  */
 import React, { useEffect, useState } from 'react';
-import rison from 'rison';
-import moment from 'moment';
+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 { createBatchMethod, createErrorHandler } from '../utils';
+import SubMenu from 'src/components/Menu/SubMenu';
+import { reject } from 'lodash';
+import { getBatchData, createErrorHandler } from '../utils';
+import EmptyState from './EmptyState';
 
 interface MapProps {
   action?: string;
@@ -35,88 +39,129 @@ interface MapProps {
   label: string;
   id: string;
   table: object;
+  item_url: string;
 }
 
 interface ActivityProps {
   user: {
-    userId: string | number;
+    userId: string;
   };
-  activityFilter: string;
 }
 
-export default function ActivityTable({ user, activityFilter }: ActivityProps) 
{
-  const [active, setActiveState] = useState([]);
-  const [loading, setLoading] = useState(false);
-  // this API uses Log for data which in some cases is can be empty
-  // const recent = `/superset/recent_activity/${user.userId}/?limit=5`;
-  const filters = {
-    // Chart and dashbaord uses same filters
-    // for edited and created
-    edited: [
-      {
-        col: 'changed_by',
-        opr: 'rel_o_m',
-        value: `${user.userId}`,
-      },
-    ],
-    created: [
-      {
-        col: 'created_by',
-        opr: 'rel_o_m',
-        value: `${user.userId}`,
-      },
-    ],
-  };
+interface ActivityData {
+  Created: Array<object>;
+  Edited: Array<object>;
+  Viewed: Array<object>;
+  Examples: Array<object>;
+}
 
-  const setBatchData = (q: string, created?: string) => {
-    createBatchMethod(q, created)
-      .then((res: Array<object>) =>
-        // @ts-ignore
-        setActiveState(res),
-      )
-      .catch(() => addDangerToast('Oops something went wrong'));
-  };
+const ActivityContainer = styled.div`
+  margin-left: 10px;
+  margin-top: -15px;
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(31%, 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: 5px;
+  }
+  .ant-card-meta-title {
+    font-weight: 700;
+  }
+`;
+
+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 is can be empty
+  const recent = `/superset/recent_activity/${user.userId}/?limit=5`;
 
   const getFilterTitle = (e: MapProps) => {
     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: MapProps) => {
     if (e.sql) return 'sql';
-    if (e.url.indexOf('dashboard') !== -1) {
+    if (e.url?.indexOf('dashboard') !== -1) {
       return 'nav-dashboard';
     }
-    if (e.url.indexOf('explore') !== -1) {
+    if (e.url?.indexOf('explore') !== -1) {
+      return 'nav-charts';
+    }
+    if (e.item_url?.indexOf('explore') !== -1) {
       return 'nav-charts';
     }
     return '';
   };
 
-  const getData = () => {
-    const queryParams = rison.encode({
-      order_column: 'changed_on_delta_humanized',
-      order_direction: 'desc',
-      page: 0,
-      page_size: 0,
-      filters: activityFilter !== 'Created' ? filters.edited : filters.created,
+  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');
+      },
     });
-    if (activityFilter === 'Edited') {
-      setBatchData(queryParams);
-    }
-    if (activityFilter === 'Created') {
-      setBatchData(queryParams, 'createdBy');
-    }
-  };
+  } else {
+    tabs.unshift({
+      name: 'Examples',
+      label: t('Examples'),
+      onClick: () => {
+        setActiveChild('Examples');
+      },
+    });
+  }
 
   useEffect(() => {
-    getData();
-  }, [activityFilter]);
+    getBatchData(user.userId, recent)
+      .then(r => {
+        const data: any = {
+          Created: [...r.createdByChart, ...r.createdByDash],
+          Edited: [...r.editedChart, ...r.editedDash],
+        };
+        if (r.viewed) {
+          const filtered = reject(r.viewed, ['item_url', null]).map(r => r);
+          data.Viewed = filtered;
+          setActiveChild('Viewed');
+        } else {
+          data.Examples = r.examples;
+          setActiveChild('Examples');
+        }
+        setActivityData(data);
+        setLoading(false);
+      })
+      .catch(e => {
+        setLoading(false);
+        addDangerToast(`e ${e}`);
+      });
+  }, []);
 
   const renderActivity = () => {
-    return active.map((e: MapProps, i) => (
+    return activityData[activeChild].map((e: MapProps, i: any) => (
       <ListViewCard
         key={`${i}`}
         isRecent
@@ -125,12 +170,32 @@ export default function ActivityTable({ user, 
activityFilter }: ActivityProps) {
         imgFallbackURL=""
         url={e.sql ? `/supserset/sqllab?queryId=${e.id}` : e.url}
         title={getFilterTitle(e)}
-        description={moment.utc(e.changed_on_utc).fromNow()}
+        description={`Last Edited: ${moment(e.changed_on_utc).format(
+          'MM/DD/YYYY HH:mm:ss',
+        )}`}
         avatar={getIconName(e)}
         actions={null}
       />
     ));
   };
-
-  return <> {renderActivity()} </>;
+  if (loading) return <>loading ...</>;
+  return (
+    <>
+      <>
+        <SubMenu
+          activeChild={activeChild}
+          name=""
+          // eslint-disable-next-line react/no-children-prop
+          children={tabs}
+        />
+        <>
+          {activityData[activeChild]?.length > 0 ? (
+            <ActivityContainer>{renderActivity()}</ActivityContainer>
+          ) : (
+            <EmptyState tableName="RECENTS" tab="Mine" />
+          )}
+        </>
+      </>
+    </>
+  );
 }
diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx 
b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
index d0ccaee..99d51c0 100644
--- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx
@@ -16,15 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect } from 'react';
+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 Owner from 'src/types/Owner';
+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;
 
@@ -37,7 +40,6 @@ interface ChartTableProps {
 }
 
 function ChartTable({
-  chartFilter,
   user,
   addDangerToast,
   addSuccessToast,
@@ -57,6 +59,8 @@ function ChartTable({
     closeChartEditModal,
   } = useChartEditModal(setCharts, charts);
 
+  const [chartFilter, setChartFilter] = useState('Favorite');
+
   const getFilters = () => {
     const filters = [];
 
@@ -110,19 +114,64 @@ function ChartTable({
         />
       )}
 
-      {charts.map((e, i) => (
-        <ChartCard
-          key={`${i}`}
-          openChartEditModal={openChartEditModal}
-          loading={loading}
-          chart={e}
-          hasPerm={hasPerm}
-          bulkSelectEnabled={bulkSelectEnabled}
-          refreshData={refreshData}
-          addDangerToast={addDangerToast}
-          addSuccessToast={addSuccessToast}
-        />
-      ))}
+      <SubMenu
+        activeChild={chartFilter}
+        name=""
+        // eslint-disable-next-line react/no-children-prop
+        children={[
+          {
+            name: 'Favorite',
+            label: t('Favorite'),
+            onClick: () => setChartFilter('Favorite'),
+          },
+          {
+            name: 'Mine',
+            label: t('Mine'),
+            onClick: () => setChartFilter('Mine'),
+          },
+        ]}
+        buttons={[
+          {
+            name: (
+              <IconContainer>
+                <Icon name="plus-small" /> Chart{' '}
+              </IconContainer>
+            ),
+            buttonStyle: 'tertiary',
+            onClick: () => {
+              // @ts-ignore
+              window.location = '/chart/add';
+            },
+          },
+          {
+            name: 'View All »',
+            buttonStyle: 'link',
+            onClick: () => {
+              // @ts-ignore
+              window.location = '/chart/list';
+            },
+          },
+        ]}
+      />
+      {charts.length ? (
+        <CardContainer>
+          {charts.map((e, i) => (
+            <ChartCard
+              key={`${i}`}
+              openChartEditModal={openChartEditModal}
+              loading={loading}
+              chart={e}
+              hasPerm={hasPerm}
+              bulkSelectEnabled={bulkSelectEnabled}
+              refreshData={refreshData}
+              addDangerToast={addDangerToast}
+              addSuccessToast={addSuccessToast}
+            />
+          ))}
+        </CardContainer>
+      ) : (
+        <EmptyState tableName="CHARTS" tab={chartFilter} />
+      )}
     </>
   );
 }
diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx 
b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
index c47b2dd..ae3daeb 100644
--- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx
@@ -16,30 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useEffect } from 'react';
-import { t } from '@superset-ui/core';
-import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
+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 Owner from 'src/types/Owner';
-import { DashboardTableProps } from 'src/views/CRUD/types';
+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 = 3;
 
-interface Dashboard {
-  changed_by_name: string;
-  changed_by_url: string;
-  changed_on_delta_humanized: string;
-  changed_by: string;
-  dashboard_title: string;
-  id: number;
-  published: boolean;
-  url: string;
-  thumbnail_url: string;
-  owners: Owner[];
-  loading: boolean;
-}
-
 export interface FilterValue {
   col: string;
   operator: string;
@@ -47,14 +37,13 @@ export interface FilterValue {
 }
 
 function DashboardTable({
-  dashboardFilter,
   user,
   addDangerToast,
   addSuccessToast,
-  search,
 }: DashboardTableProps) {
   const {
     state: { loading, resourceCollection: dashboards, bulkSelectEnabled },
+    setResourceCollection: setDashboards,
     hasPerm,
     refreshData,
     fetchData,
@@ -64,6 +53,31 @@ function DashboardTable({
     addDangerToast,
   );
 
+  const [editModal, setEditModal] = useState<Dashboard | null>(null);
+  const [dashboardFilter, setDashboardFilter] = useState('Favorite');
+
+  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),
+        ),
+      ),
+    );
+  };
+
   const getFilters = () => {
     const filters = [];
 
@@ -80,15 +94,16 @@ function DashboardTable({
         value: true,
       });
     }
-    filters.concat([
-      {
-        id: 'dashboard_title',
-        operator: 'ct',
-        value: search,
-      },
-    ]);
     return filters;
   };
+  const subMenus = [];
+  if (dashboards.length > 0 && dashboardFilter === 'favorite') {
+    subMenus.push({
+      name: 'Favorite',
+      label: t('Favorite'),
+      onClick: () => setDashboardFilter('Favorite'),
+    });
+  }
 
   useEffect(() => {
     fetchData({
@@ -106,18 +121,73 @@ function DashboardTable({
 
   return (
     <>
-      {dashboards.map(e => (
-        <DashboardCard
-          {...{
-            dashboard: e,
-            hasPerm,
-            bulkSelectEnabled,
-            refreshData,
-            addDangerToast,
-            addSuccessToast,
-          }}
+      <SubMenu
+        activeChild={dashboardFilter}
+        name=""
+        // eslint-disable-next-line react/no-children-prop
+        children={[
+          {
+            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>
+            ),
+            buttonStyle: 'tertiary',
+            onClick: () => {
+              // @ts-ignore
+              window.location = '/dashboard/new';
+            },
+          },
+          {
+            name: 'View All »',
+            buttonStyle: 'link',
+            onClick: () => {
+              // @ts-ignore
+              window.location = '/dashboard/list/';
+            },
+          },
+        ]}
+      />
+      {editModal && (
+        <PropertiesModal
+          dashboardId={editModal?.id}
+          show
+          onHide={() => setEditModal(null)}
+          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} />
+      )}
     </>
   );
 }
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..e1c2936
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/welcome/EmptyState.tsx
@@ -0,0 +1,112 @@
+/**
+ * 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 { styled } from '@superset-ui/core';
+import Icon from 'src/components/Icon';
+import { IconContainer } from '../utils';
+
+interface EmptyStateProps {
+  tableName: string;
+  tab?: string;
+}
+
+const Container = styled.div`
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  img {
+    width: 87px;
+    display: block;
+    margin: 0 auto;
+  }
+  div:nth-child(2) {
+    text-align: center;
+    margin-top: 15px;
+    color: ${({ theme }) => theme.colors.grayscale.dark1};
+    font-weight: 400;
+  }
+  button {
+    margin: 0 auto;
+    padding: 6px 27px;
+    margin-top: 10px;
+    svg {
+      color: white;
+    }
+  }
+`;
+
+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 Mine = (
+    <div>
+      <div>{`No ${tableName.toLowerCase()} yet`}</div>
+      <Button
+        buttonStyle="primary"
+        onClick={() => {
+          window.location = mineRedirects[tableName];
+        }}
+      >
+        <IconContainer>
+          <Icon name="plus-small" /> {tableName}
+        </IconContainer>
+      </Button>
+    </div>
+  );
+  const span = (
+    <div>
+      Recently viewed charts, dashboards, and saved queries will appear here
+    </div>
+  );
+
+  if (tab === 'Mine') {
+    return (
+      <Container>
+        <img src="/static/assets/images/union.png" alt="union.png" />
+        {tableName === 'RECENTS' ? span : Mine}
+      </Container>
+    );
+  }
+
+  return (
+    <Container>
+      <img src="/static/assets/images/star-circle.png" alt="star.png" />
+      <div>
+        <div>You don't have any favorites yets!</div>
+        <Button
+          buttonStyle="primary"
+          onClick={() => {
+            window.location = favRedirects[tableName];
+          }}
+        >
+          SEE ALL {tableName}
+        </Button>
+      </div>
+    </Container>
+  );
+}
diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx 
b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
index c60d4fb..333d394 100644
--- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx
@@ -17,18 +17,24 @@
  * under the License.
  */
 import React, { useEffect, useState } from 'react';
-import { t } from '@superset-ui/core';
+import { t, styled, SupersetClient } from '@superset-ui/core';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import { Dropdown, Menu } from 'src/common/components';
-import { useListViewResource } from 'src/views/CRUD/hooks';
+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 { addDangerToast } from 'src/messageToasts/actions';
+import { SavedQueryObject } from 'src/views/CRUD/types';
+import SubMenu from 'src/components/Menu/SubMenu';
+import EmptyState from './EmptyState';
+
+import { IconContainer, CardContainer, createErrorHandler } from '../utils';
 
 const PAGE_SIZE = 3;
 
 interface Query {
-  sql_tables: array;
+  id: number;
+  sql_tables: Array<any>;
   database: {
     database_name: string;
   };
@@ -36,6 +42,7 @@ interface Query {
   description: string;
   end_time: string;
   addDangerToast: () => void;
+  label: string;
 }
 
 interface SavedQueriesProps {
@@ -45,11 +52,46 @@ interface SavedQueriesProps {
   queryFilter: string;
 }
 
-const SavedQueries = ({ user, queryFilter }: SavedQueriesProps) => {
+const NoData = styled.div`
+  .create-your-query {
+    display: block;
+    margin: 0 auto;
+  }
+`;
+
+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('Favorite');
+  const [queryDeleteModal, setQueryDeleteModal] = useState(false);
+  const [currentlyEdited, setCurrentlyEdited] = useState(null);
+
+  const canEdit = hasPerm('can_edit');
+  const canDelete = hasPerm('can_delete');
+
+  const handleQueryDelete = ({ id, label }: SavedQueryObject) => {
+    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 = [];
 
@@ -83,36 +125,117 @@ const SavedQueries = ({ user, queryFilter }: 
SavedQueriesProps) => {
     });
   }, [queryFilter]);
 
-  const menu = (
+  const renderMenu = (query: Query) => (
     <Menu>
-      <Menu.Item>Delete</Menu.Item>
+      {canEdit && (
+        <Menu.Item
+          onClick={() => {
+            // @ts-ignore
+            window.location = `/superset/sqllab?savedQueryId=${query.id}`;
+          }}
+        >
+          Edit
+        </Menu.Item>
+      )}
+      <Menu.Item
+        onClick={() => copyQueryLink(query.id, addDangerToast, 
addSuccessToast)}
+      >Share</Menu.Item>
+      {canDelete && (
+        <Menu.Item
+          onClick={() => {
+            setQueryDeleteModal(true);
+            setCurrentlyEdited(query);
+          }}
+        >
+          Delete
+        </Menu.Item>
+      )}
     </Menu>
   );
 
   return (
     <>
-      {queries ? (
-        queries.map(q => (
-          <ListViewCard
-            imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
-            imgURL=""
-            title={q.database.database_name}
-            rows={q.rows}
-            tableName={q.sql_tables[0].table}
-            loading={loading}
-            description={t('Last run ', q.end_time)}
-            showImg={false}
-            actions={
-              <ListViewCard.Actions>
-                <Dropdown overlay={menu}>
-                  <Icon name="more-horiz" />
-                </Dropdown>
-              </ListViewCard.Actions>
+      {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}
+        name=""
+        // eslint-disable-next-line react/no-children-prop
+        children={[
+          {
+            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: () => {
+              // @ts-ignore
+              window.location = '/superset/sqllab';
+            },
+          },
+          {
+            name: 'View All »',
+            buttonStyle: 'link',
+            onClick: () => {
+              // @ts-ignore
+              window.location = '/savedqueryview/list';
+            },
+          },
+        ]}
+      />
+      {queries.length > 0 ? (
+        <CardContainer>
+          {queries.map(q => (
+            <ListViewCard
+              imgFallbackURL=""
+              imgURL=""
+              title={q.label}
+              rows={q.rows}
+              tableName={q.sql_tables[0]?.table}
+              tables={q.sql_tables?.length}
+              loading={loading}
+              description={t('Last run ', q.end_time)}
+              showImg={false}
+              actions={
+                <ListViewCard.Actions>
+                  <Dropdown overlay={renderMenu(q)}>
+                    <Icon name="more-horiz" />
+                  </Dropdown>
+                </ListViewCard.Actions>
+              }
+            />
+          ))}
+        </CardContainer>
       ) : (
-        <span>You have no Saved Queries!</span>
+        <EmptyState tableName="SAVED_QUERIES" tab={queryFilter} />
       )}
     </>
   );
diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx 
b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
index f0934e1..7f7464b 100644
--- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
+++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx
@@ -16,8 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { useState } from 'react';
-import SubMenu from 'src/components/Menu/SubMenu';
+import React from 'react';
 import { styled, t } from '@superset-ui/core';
 import { Collapse } from 'src/common/components';
 import { User } from 'src/types/bootstrapTypes';
@@ -35,213 +34,57 @@ interface WelcomeProps {
 }
 
 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 26px;
+      position: relative;
+      top: -13px;
+    }
+    .nav.navbar-nav {
+      & > li:nth-child(1),
+      & > li:nth-child(2),
+      & > li:nth-child(3) {
+        margin-top: 8px;
+      }
+    }
+    button {
+      padding: 3px 21px;
+    }
+    .navbar-right {
+      position: relative;
+      top: 11px;
     }
   }
   .ant-card.ant-card-bordered {
     border: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
   }
-`;
-
-const ActivityContainer = styled.div`
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(31%, max-content));
-  grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
-  justify-content: center;
-  padding: ${({ theme }) => theme.gridUnit * 2}px
-    ${({ theme }) => theme.gridUnit * 4}px;
-`;
-
-const IconContainer = styled.div`
-  svg {
-    vertical-align: -7px;
-    color: ${({ theme }) => theme.colors.primary.dark1};
+  .ant-collapse-header {
+    font-weight: 500;
+    font-size: 16px;
   }
 `;
-export const CardContainer = styled.div`
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(459px, 1fr));
-  grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
-  justify-content: left;
-  padding: ${({ theme }) => theme.gridUnit * 2}px
-    ${({ theme }) => theme.gridUnit * 6}px;
-`;
 
 export default function Welcome({ user }: WelcomeProps) {
-  const [queryFilter, setQueryFilter] = useState('Favorite');
-  const [activityFilter, setActivityFilter] = useState('Edited');
-  const [dashboardFilter, setDashboardFilter] = useState('Favorite');
-  const [chartFilter, setChartFilter] = useState('Favorite');
-
-  function ExpandIcon(): React.ReactNode {
-    return <Icon name="caret-right" />;
-  }
-
   return (
     <WelcomeContainer>
       <Collapse defaultActiveKey={['1']} ghost>
         <Panel header={t('Recents')} key="1">
-          <SubMenu
-            activeChild={activityFilter}
-            name=""
-            // eslint-disable-next-line react/no-children-prop
-            children={[
-              {
-                name: 'Edited',
-                label: t('Edited'),
-                onClick: () => setActivityFilter('Edited'),
-              },
-              {
-                name: 'Created',
-                label: t('Created'),
-                onClick: () => setActivityFilter('Created'),
-              },
-            ]}
-          />
-          <ActivityContainer>
-            <ActivityTable user={user} activityFilter={activityFilter} />
-          </ActivityContainer>
+          <ActivityTable user={user} />
         </Panel>
-
-        <Panel header={t('Dashboards')} key="2">
-          <SubMenu
-            activeChild={dashboardFilter}
-            name=""
-            // eslint-disable-next-line react/no-children-prop
-            children={[
-              {
-                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>
-                ),
-                buttonStyle: 'tertiary',
-                onClick: () => {
-                  // @ts-ignore
-                  window.location = '/dashboard/new';
-                },
-              },
-              {
-                name: 'View All »',
-                buttonStyle: 'link',
-                onClick: () => {
-                  // @ts-ignore
-                  window.location = '/dashboard/list/';
-                },
-              },
-            ]}
-          />
-          <CardContainer>
-            <DashboardTable dashboardFilter={dashboardFilter} user={user} />
-          </CardContainer>
+        <Panel header={t('Dashboards')} key="1">
+          <DashboardTable user={user} />
         </Panel>
-
-        <Panel header={t('Saved Queries')} key="3">
-          <SubMenu
-            activeChild={queryFilter}
-            name=""
-            // eslint-disable-next-line react/no-children-prop
-            children={[
-              {
-                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: () => {
-                  // @ts-ignore
-                  window.location = '/superset/sqllab';
-                },
-              },
-              {
-                name: 'View All »',
-                buttonStyle: 'link',
-                onClick: () => {
-                  // @ts-ignore
-                  window.location = '/savedqueryview/list';
-                },
-              },
-            ]}
-          />
-          <CardContainer>
-            <SavedQueries user={user} queryFilter={queryFilter} />
-          </CardContainer>
+        <Panel header={t('Saved Queries')} key="1">
+          <SavedQueries user={user} />
         </Panel>
-        <Panel header={t('Charts')} key="4">
-          <SubMenu
-            activeChild={chartFilter}
-            name=""
-            // eslint-disable-next-line react/no-children-prop
-            children={[
-              {
-                name: 'Favorite',
-                label: t('Favorite'),
-                onClick: () => setChartFilter('Favorite'),
-              },
-              {
-                name: 'Mine',
-                label: t('Mine'),
-                onClick: () => setChartFilter('Mine'),
-              },
-            ]}
-            buttons={[
-              {
-                name: (
-                  <IconContainer>
-                    <Icon name="plus-small" /> Chart{' '}
-                  </IconContainer>
-                ),
-                buttonStyle: 'tertiary',
-                onClick: () => {
-                  // @ts-ignore
-                  window.location = '/chart/add';
-                },
-              },
-              {
-                name: 'View All »',
-                buttonStyle: 'link',
-                onClick: () => {
-                  // @ts-ignore
-                  window.location = '/chart/list';
-                },
-              },
-            ]}
-          />
-
-          <CardContainer>
-            <ChartTable chartFilter={chartFilter} user={user} />
-          </CardContainer>
+        <Panel header={t('Charts')} key="1">
+          <ChartTable user={user} />
         </Panel>
       </Collapse>
     </WelcomeContainer>

Reply via email to