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

tai 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 db88cec  feat: SIP-34 card/grid views for dashboards and charts  
(#10526)
db88cec is described below

commit db88cec431bca04608c6580192714da03bed1e1b
Author: ʈᵃᵢ <[email protected]>
AuthorDate: Thu Aug 13 14:46:56 2020 -0700

    feat: SIP-34 card/grid views for dashboards and charts  (#10526)
---
 superset-frontend/.storybook/main.js               |   2 +-
 superset-frontend/images/chart-card-fallback.png   | Bin 0 -> 3183 bytes
 .../images/dashboard-card-fallback.png             | Bin 0 -> 2621 bytes
 superset-frontend/images/icons/card-view.svg       |  21 ++
 superset-frontend/images/icons/list-view.svg       |  21 ++
 superset-frontend/images/icons/search.svg          |   2 +-
 .../views/CRUD/chart/ChartList_spec.jsx            |  19 +-
 .../views/CRUD/dashboard/DashboardList_spec.jsx    |  13 +-
 .../views/CRUD/dataset/DatasetList_spec.jsx        |   2 +-
 .../javascripts/welcome/DashboardTable_spec.tsx    |   2 +-
 superset-frontend/src/components/AvatarIcon.tsx    |  14 +-
 superset-frontend/src/components/FaveStar.tsx      |   2 +-
 superset-frontend/src/components/Icon/index.tsx    |  39 ++-
 superset-frontend/src/components/Label/index.tsx   |   7 +
 .../src/components/ListView/CardCollection.tsx     |  56 ++++
 .../src/components/ListView/Filters.tsx            |   5 +-
 .../src/components/ListView/ListView.tsx           | 326 +++++++++------------
 .../src/components/ListView/TableCollection.tsx    | 114 ++++++-
 .../Chart.ts => components/ListView/index.ts}      |  16 +-
 .../ListViewCard/ListViewCard.stories.tsx          |  75 +++++
 .../src/components/ListViewCard/index.tsx          | 197 +++++++++++++
 superset-frontend/src/components/Pagination.tsx    |   1 +
 superset-frontend/src/components/SearchInput.tsx   |   6 +-
 .../src/explore/components/PropertiesModal.tsx     |   1 +
 superset-frontend/src/types/Chart.ts               |   6 +
 superset-frontend/src/types/{Chart.ts => Owner.ts} |  16 +-
 .../src/views/CRUD/chart/ChartList.tsx             |  96 +++++-
 .../src/views/CRUD/dashboard/DashboardList.tsx     | 216 ++++++++++----
 .../src/views/CRUD/dataset/DatasetList.tsx         |  20 +-
 superset-frontend/src/welcome/DashboardTable.tsx   |   3 +-
 superset/charts/api.py                             |   6 +
 31 files changed, 976 insertions(+), 328 deletions(-)

diff --git a/superset-frontend/.storybook/main.js 
b/superset-frontend/.storybook/main.js
index 2b94128..49921cf 100644
--- a/superset-frontend/.storybook/main.js
+++ b/superset-frontend/.storybook/main.js
@@ -22,7 +22,7 @@ const path = require('path');
 const customConfig = require('../webpack.config.js');
 
 module.exports = {
-  stories: ['../src/components/**/*.stories.jsx'],
+  stories: ['../src/components/**/*.stories.(t|j)sx'],
   addons: [
     '@storybook/addon-actions',
     '@storybook/addon-links',
diff --git a/superset-frontend/images/chart-card-fallback.png 
b/superset-frontend/images/chart-card-fallback.png
new file mode 100644
index 0000000..aa34d4f
Binary files /dev/null and b/superset-frontend/images/chart-card-fallback.png 
differ
diff --git a/superset-frontend/images/dashboard-card-fallback.png 
b/superset-frontend/images/dashboard-card-fallback.png
new file mode 100644
index 0000000..1b4e968
Binary files /dev/null and 
b/superset-frontend/images/dashboard-card-fallback.png differ
diff --git a/superset-frontend/images/icons/card-view.svg 
b/superset-frontend/images/icons/card-view.svg
new file mode 100644
index 0000000..009409b
--- /dev/null
+++ b/superset-frontend/images/icons/card-view.svg
@@ -0,0 +1,21 @@
+<!--
+  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.
+-->
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 4C3.22386 4 3 4.22386 3 
4.5V7.5C3 7.77614 3.22386 8 3.5 8H10.5C10.7761 8 11 7.77614 11 7.5V4.5C11 
4.22386 10.7761 4 10.5 4H3.5ZM13.5 4C13.2239 4 13 4.22386 13 4.5V7.5C13 7.77614 
13.2239 8 13.5 8H20.5C20.7761 8 21 7.77614 21 7.5V4.5C21 4.22386 20.7761 4 20.5 
4H13.5ZM3 10.5C3 10.2239 3.22386 10 3.5 10H10.5C10.7761 10 11 10.2239 11 
10.5V13.5C11 13.7761 10.7761 14 10.5 14H3.5C3.22386 14 3 13.7761 3 
13.5V10.5ZM3.5 16C3.22386 16 3 16.2239 3 [...]
+</svg>
diff --git a/superset-frontend/images/icons/list-view.svg 
b/superset-frontend/images/icons/list-view.svg
new file mode 100644
index 0000000..9d33b74
--- /dev/null
+++ b/superset-frontend/images/icons/list-view.svg
@@ -0,0 +1,21 @@
+<!--
+  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.
+-->
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3.71023 16.29C3.61513 16.199 
3.50298 16.1276 3.38023 16.08C3.13677 15.98 2.86369 15.98 2.62023 16.08C2.49748 
16.1276 2.38534 16.199 2.29023 16.29C2.19919 16.3851 2.12783 16.4972 2.08023 
16.62C1.92364 16.9924 2.00649 17.4224 2.29023 17.71C2.38743 17.7983 2.49905 
17.8694 2.62023 17.92C2.86227 18.027 3.13819 18.027 3.38023 17.92C3.50142 
17.8694 3.61303 17.7983 3.71023 17.71C3.99398 17.4224 4.07683 16.9924 3.92023 
16.62C3.87264 16.4972 3.8012 [...]
+</svg>
diff --git a/superset-frontend/images/icons/search.svg 
b/superset-frontend/images/icons/search.svg
index 5b86f13..bef0709 100644
--- a/superset-frontend/images/icons/search.svg
+++ b/superset-frontend/images/icons/search.svg
@@ -16,7 +16,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" 
xmlns="http://www.w3.org/2000/svg"; xmlns:xlink="http://www.w3.org/1999/xlink";>
+<svg width="24" height="24" viewBox="0 0 24 24" version="1.1" 
xmlns="http://www.w3.org/2000/svg"; xmlns:xlink="http://www.w3.org/1999/xlink";>
     <!-- Generator: Sketch 64 (93537) - https://sketch.com -->
     <title>Icon / [email protected]</title>
     <desc>Created with Sketch.</desc>
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 cab27f7..2083b4a 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
@@ -24,7 +24,9 @@ import fetchMock from 'fetch-mock';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
 import ChartList from 'src/views/CRUD/chart/ChartList';
-import ListView from 'src/components/ListView/ListView';
+import ListView from 'src/components/ListView';
+import PropertiesModal from 'src/explore/components/PropertiesModal';
+import ListViewCard from 'src/components/ListViewCard';
 
 // store needed for withToasts(ChartTable)
 const mockStore = configureStore([thunk]);
@@ -96,4 +98,19 @@ describe('ChartList', () => {
       
`"http://localhost/api/v1/chart/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
     );
   });
+
+  it('renders a card view', () => {
+    expect(wrapper.find(ListViewCard)).toExist();
+  });
+
+  it('renders a table view', () => {
+    wrapper.find('[data-test="list-view"]').first().simulate('click');
+    expect(wrapper.find('table')).toExist();
+  });
+
+  it('edits', () => {
+    expect(wrapper.find(PropertiesModal)).not.toExist();
+    wrapper.find('[data-test="pencil"]').first().simulate('click');
+    expect(wrapper.find(PropertiesModal)).toExist();
+  });
 });
diff --git 
a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
 
b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
index 5c448ea..eef4ca0 100644
--- 
a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
+++ 
b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
@@ -24,8 +24,9 @@ import fetchMock from 'fetch-mock';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
 import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
-import ListView from 'src/components/ListView/ListView';
+import ListView from 'src/components/ListView';
 import PropertiesModal from 'src/dashboard/components/PropertiesModal';
+import ListViewCard from 'src/components/ListViewCard';
 
 // store needed for withToasts(DashboardTable)
 const mockStore = configureStore([thunk]);
@@ -88,6 +89,16 @@ describe('DashboardList', () => {
       
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
     );
   });
+
+  it('renders a card view', () => {
+    expect(wrapper.find(ListViewCard)).toExist();
+  });
+
+  it('renders a table view', () => {
+    wrapper.find('[data-test="list-view"]').first().simulate('click');
+    expect(wrapper.find('table')).toExist();
+  });
+
   it('edits', () => {
     expect(wrapper.find(PropertiesModal)).not.toExist();
     wrapper.find('[data-test="pencil"]').first().simulate('click');
diff --git 
a/superset-frontend/spec/javascripts/views/CRUD/dataset/DatasetList_spec.jsx 
b/superset-frontend/spec/javascripts/views/CRUD/dataset/DatasetList_spec.jsx
index 01fb2e8..53de35d 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/dataset/DatasetList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/dataset/DatasetList_spec.jsx
@@ -24,7 +24,7 @@ import fetchMock from 'fetch-mock';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
 import DatasetList from 'src/views/CRUD/dataset/DatasetList';
-import ListView from 'src/components/ListView/ListView';
+import ListView from 'src/components/ListView';
 import Button from 'src/components/Button';
 import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
 import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
diff --git a/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.tsx 
b/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.tsx
index d8f4f2b..b612fbb 100644
--- a/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.tsx
+++ b/superset-frontend/spec/javascripts/welcome/DashboardTable_spec.tsx
@@ -23,7 +23,7 @@ import configureStore from 'redux-mock-store';
 import fetchMock from 'fetch-mock';
 import { supersetTheme, ThemeProvider } from '@superset-ui/style';
 
-import ListView from 'src/components/ListView/ListView';
+import ListView from 'src/components/ListView';
 import DashboardTable from 'src/welcome/DashboardTable';
 
 // store needed for withToasts(DashboardTable)
diff --git a/superset-frontend/src/components/AvatarIcon.tsx 
b/superset-frontend/src/components/AvatarIcon.tsx
index 7dcf8f7..f1a2fd3 100644
--- a/superset-frontend/src/components/AvatarIcon.tsx
+++ b/superset-frontend/src/components/AvatarIcon.tsx
@@ -17,7 +17,6 @@
  * under the License.
  */
 import React from 'react';
-import styled from '@superset-ui/style';
 import { getCategoricalSchemeRegistry } from '@superset-ui/color';
 import Avatar, { ConfigProvider } from 'react-avatar';
 import TooltipWrapper from 'src/components/TooltipWrapper';
@@ -25,27 +24,20 @@ import TooltipWrapper from 'src/components/TooltipWrapper';
 interface Props {
   firstName: string;
   lastName: string;
-  tableName: string;
-  userName: string;
+  uniqueKey: string;
   iconSize: number;
   textSize: number;
 }
 
 const colorList = getCategoricalSchemeRegistry().get();
 
-const StyledAvatar = styled(Avatar)`
-  margin: 0px 5px;
-`;
-
 export default function AvatarIcon({
-  tableName,
+  uniqueKey,
   firstName,
   lastName,
-  userName,
   iconSize,
   textSize,
 }: Props) {
-  const uniqueKey = `${tableName}-${userName}`;
   const fullName = `${firstName} ${lastName}`;
 
   return (
@@ -55,7 +47,7 @@ export default function AvatarIcon({
       tooltip={fullName}
     >
       <ConfigProvider colors={colorList && colorList.colors}>
-        <StyledAvatar
+        <Avatar
           key={uniqueKey}
           name={fullName}
           size={String(iconSize)}
diff --git a/superset-frontend/src/components/FaveStar.tsx 
b/superset-frontend/src/components/FaveStar.tsx
index c135ceb..dc1f6e8 100644
--- a/superset-frontend/src/components/FaveStar.tsx
+++ b/superset-frontend/src/components/FaveStar.tsx
@@ -61,7 +61,7 @@ export default class FaveStar extends 
React.PureComponent<FaveStarProps> {
               }
               viewBox="0 0 16 15"
               width={this.props.width || 20}
-              height="auto"
+              height={this.props.height || 'auto'}
             />
           </a>
         </TooltipWrapper>
diff --git a/superset-frontend/src/components/Icon/index.tsx 
b/superset-frontend/src/components/Icon/index.tsx
index a0d516d..c325c81 100644
--- a/superset-frontend/src/components/Icon/index.tsx
+++ b/superset-frontend/src/components/Icon/index.tsx
@@ -18,12 +18,13 @@
  */
 import React, { SVGProps } from 'react';
 import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
-import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
-import { ReactComponent as CircleCheckIcon } from 
'images/icons/circle-check.svg';
-import { ReactComponent as CircleCheckSolidIcon } from 
'images/icons/circle-check-solid.svg';
+import { ReactComponent as CardViewIcon } from 'images/icons/card-view.svg';
 import { ReactComponent as CheckboxHalfIcon } from 
'images/icons/checkbox-half.svg';
 import { ReactComponent as CheckboxOffIcon } from 
'images/icons/checkbox-off.svg';
 import { ReactComponent as CheckboxOnIcon } from 
'images/icons/checkbox-on.svg';
+import { ReactComponent as CheckIcon } from 'images/icons/check.svg';
+import { ReactComponent as CircleCheckIcon } from 
'images/icons/circle-check.svg';
+import { ReactComponent as CircleCheckSolidIcon } from 
'images/icons/circle-check-solid.svg';
 import { ReactComponent as CloseIcon } from 'images/icons/close.svg';
 import { ReactComponent as CompassIcon } from 'images/icons/compass.svg';
 import { ReactComponent as DatasetPhysicalIcon } from 
'images/icons/dataset_physical.svg';
@@ -31,39 +32,42 @@ import { ReactComponent as DatasetVirtualIcon } from 
'images/icons/dataset_virtu
 import { ReactComponent as ErrorIcon } from 'images/icons/error.svg';
 import { ReactComponent as FavoriteSelectedIcon } from 
'images/icons/favorite-selected.svg';
 import { ReactComponent as FavoriteUnselectedIcon } from 
'images/icons/favorite-unselected.svg';
-import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg';
+import { ReactComponent as ListViewIcon } from 'images/icons/list-view.svg';
 import { ReactComponent as MoreIcon } from 'images/icons/more.svg';
+import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg';
 import { ReactComponent as SearchIcon } from 'images/icons/search.svg';
+import { ReactComponent as ShareIcon } from 'images/icons/share.svg';
 import { ReactComponent as SortAscIcon } from 'images/icons/sort-asc.svg';
 import { ReactComponent as SortDescIcon } from 'images/icons/sort-desc.svg';
 import { ReactComponent as SortIcon } from 'images/icons/sort.svg';
 import { ReactComponent as TrashIcon } from 'images/icons/trash.svg';
 import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
-import { ReactComponent as ShareIcon } from 'images/icons/share.svg';
 
 type IconName =
   | 'cancel-x'
+  | 'card-view'
   | 'check'
   | 'checkbox-half'
   | 'checkbox-off'
   | 'checkbox-on'
-  | 'close'
-  | 'circle-check'
   | 'circle-check-solid'
+  | 'circle-check'
+  | 'close'
   | 'compass'
   | 'dataset-physical'
   | 'dataset-virtual'
   | 'error'
   | 'favorite-selected'
   | 'favorite-unselected'
+  | 'list-view'
   | 'more'
   | 'pencil'
   | 'search'
-  | 'sort'
+  | 'share'
   | 'sort-asc'
   | 'sort-desc'
+  | 'sort'
   | 'trash'
-  | 'share'
   | 'warning';
 
 export const iconsRegistry: Record<
@@ -71,15 +75,17 @@ export const iconsRegistry: Record<
   React.ComponentType<SVGProps<SVGSVGElement>>
 > = {
   'cancel-x': CancelXIcon,
+  'card-view': CardViewIcon,
   'checkbox-half': CheckboxHalfIcon,
   'checkbox-off': CheckboxOffIcon,
   'checkbox-on': CheckboxOnIcon,
-  'circle-check': CircleCheckIcon,
   'circle-check-solid': CircleCheckSolidIcon,
+  'circle-check': CircleCheckIcon,
   'dataset-physical': DatasetPhysicalIcon,
   'dataset-virtual': DatasetVirtualIcon,
   'favorite-selected': FavoriteSelectedIcon,
   'favorite-unselected': FavoriteUnselectedIcon,
+  'list-view': ListViewIcon,
   'sort-asc': SortAscIcon,
   'sort-desc': SortDescIcon,
   check: CheckIcon,
@@ -89,18 +95,25 @@ export const iconsRegistry: Record<
   more: MoreIcon,
   pencil: PencilIcon,
   search: SearchIcon,
+  share: ShareIcon,
   sort: SortIcon,
   trash: TrashIcon,
   warning: WarningIcon,
-  share: ShareIcon,
 };
 interface IconProps extends SVGProps<SVGSVGElement> {
   name: IconName;
 }
 
-const Icon = ({ name, color = '#666666', ...rest }: IconProps) => {
+const Icon = ({
+  name,
+  color = '#666666',
+  viewBox = '0 0 24 24',
+  ...rest
+}: IconProps) => {
   const Component = iconsRegistry[name];
-  return <Component color={color} data-test={name} {...rest} />;
+  return (
+    <Component color={color} viewBox={viewBox} data-test={name} {...rest} />
+  );
 };
 
 export default Icon;
diff --git a/superset-frontend/src/components/Label/index.tsx 
b/superset-frontend/src/components/Label/index.tsx
index 359b95e..58c336e 100644
--- a/superset-frontend/src/components/Label/index.tsx
+++ b/superset-frontend/src/components/Label/index.tsx
@@ -63,6 +63,13 @@ const SupersetLabel = styled(BootstrapLabel)`
       background-color: ${({ theme }) => theme.colors.error.base};
     }
   }
+
+  &.secondaryLabel {
+    background-color: ${({ theme }) => theme.colors.secondary.base};
+    &:hover {
+      background-color: ${({ theme }) => theme.colors.secondary.base};
+    }
+  }
 `;
 
 export default function Label(props: LabelProps) {
diff --git a/superset-frontend/src/components/ListView/CardCollection.tsx 
b/superset-frontend/src/components/ListView/CardCollection.tsx
new file mode 100644
index 0000000..6668850
--- /dev/null
+++ b/superset-frontend/src/components/ListView/CardCollection.tsx
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { TableInstance } from 'react-table';
+import styled from '@superset-ui/style';
+
+interface Props {
+  renderCard?: (row: any) => React.ReactNode;
+  prepareRow: TableInstance['prepareRow'];
+  rows: TableInstance['rows'];
+  loading: boolean;
+}
+
+const CardContainer = styled.div`
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(459px, max-content));
+  grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
+  justify-content: center;
+  padding: ${({ theme }) => theme.gridUnit * 2}px
+    ${({ theme }) => theme.gridUnit * 4}px;
+`;
+
+export default function CardCollection({
+  renderCard,
+  prepareRow,
+  rows,
+  loading,
+}: Props) {
+  return (
+    <CardContainer>
+      {rows.map(row => {
+        if (!renderCard) return null;
+        prepareRow(row);
+        return (
+          <div key={row.id}>{renderCard({ ...row.original, loading })}</div>
+        );
+      })}
+    </CardContainer>
+  );
+}
diff --git a/superset-frontend/src/components/ListView/Filters.tsx 
b/superset-frontend/src/components/ListView/Filters.tsx
index c601768..a27a1d6 100644
--- a/superset-frontend/src/components/ListView/Filters.tsx
+++ b/superset-frontend/src/components/ListView/Filters.tsx
@@ -218,7 +218,10 @@ interface UIFiltersProps {
 }
 
 const FilterWrapper = styled.div`
-  padding: 24px 16px 8px;
+  display: inline-block;
+  padding: ${({ theme }) => theme.gridUnit * 6}px
+    ${({ theme }) => theme.gridUnit * 4}px
+    ${({ theme }) => theme.gridUnit * 2}px;
 `;
 
 function UIFilters({
diff --git a/superset-frontend/src/components/ListView/ListView.tsx 
b/superset-frontend/src/components/ListView/ListView.tsx
index 2f797e9..1a8313b 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -17,13 +17,15 @@
  * under the License.
  */
 import { t } from '@superset-ui/translation';
-import React, { FunctionComponent } from 'react';
-import { Col, Row, Alert } from 'react-bootstrap';
+import React, { FunctionComponent, useState } from 'react';
+import { Alert } from 'react-bootstrap';
 import styled from '@superset-ui/style';
 import cx from 'classnames';
 import Button from 'src/components/Button';
+import Icon from 'src/components/Icon';
 import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
 import TableCollection from './TableCollection';
+import CardCollection from './CardCollection';
 import Pagination from './Pagination';
 import FilterControls from './Filters';
 import { FetchDataConfig, Filters, SortColumn } from './types';
@@ -42,173 +44,21 @@ const ListViewStyles = styled.div`
     .body {
       overflow: scroll;
       max-height: 64vh;
-
-      table {
-        border-collapse: separate;
-
-        th {
-          background: white;
-          position: sticky;
-          top: 0;
-          &:first-of-type {
-            padding-left: ${({ theme }) => theme.gridUnit * 4}px;
-          }
-        }
-      }
-    }
-
-    .filter-dropdown {
-      margin-top: 20px;
-    }
-
-    .filter-column {
-      height: 30px;
-      padding: 5px;
-      font-size: 16px;
-    }
-
-    .filter-close {
-      height: 30px;
-      padding: 5px;
-
-      i {
-        font-size: 20px;
-      }
-    }
-
-    .table-cell-loader {
-      position: relative;
-
-      .loading-bar {
-        background-color: ${({ theme }) => theme.colors.secondary.light4};
-        border-radius: 7px;
-
-        span {
-          visibility: hidden;
-        }
-      }
-
-      &:after {
-        position: absolute;
-        transform: translateY(-50%);
-        top: 50%;
-        left: 0;
-        content: '';
-        display: block;
-        width: 100%;
-        height: 48px;
-        background-image: linear-gradient(
-          100deg,
-          rgba(255, 255, 255, 0),
-          rgba(255, 255, 255, 0.5) 60%,
-          rgba(255, 255, 255, 0) 80%
-        );
-        background-size: 200px 48px;
-        background-position: -100px 0;
-        background-repeat: no-repeat;
-        animation: loading-shimmer 1s infinite;
-      }
-    }
-
-    .actions {
-      white-space: nowrap;
-      font-size: 24px;
-      min-width: 100px;
-
-      svg,
-      i {
-        margin-right: 8px;
-
-        &:hover {
-          path {
-            fill: ${({ theme }) => theme.colors.primary.base};
-          }
-        }
-      }
-    }
-
-    .table-row {
-      .actions {
-        opacity: 0;
-      }
-
-      &:hover {
-        background-color: ${({ theme }) => theme.colors.secondary.light5};
-
-        .actions {
-          opacity: 1;
-          transition: opacity ease-in ${({ theme }) => 
theme.transitionTiming}s;
-        }
-      }
-    }
-
-    .table-row-selected {
-      background-color: ${({ theme }) => theme.colors.secondary.light4};
-
-      &:hover {
-        background-color: ${({ theme }) => theme.colors.secondary.light4};
-      }
-    }
-
-    .table-cell {
-      text-overflow: ellipsis;
-      overflow: hidden;
-      white-space: nowrap;
-      max-width: 300px;
-      line-height: 1;
-      vertical-align: middle;
-      &:first-of-type {
-        padding-left: ${({ theme }) => theme.gridUnit * 4}px;
-      }
-    }
-
-    .sort-icon {
-      position: absolute;
-    }
-
-    .form-actions-container {
-      position: absolute;
-      left: 28px;
-    }
-
-    .row-count-container {
-      float: right;
-      padding-right: 24px;
     }
   }
 
-  @keyframes loading-shimmer {
-    40% {
-      background-position: 100% 0;
-    }
+  .pagination-container {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+  }
 
-    100% {
-      background-position: 100% 0;
-    }
+  .row-count-container {
+    margin-top: ${({ theme }) => theme.gridUnit * 2}px;
+    color: ${({ theme }) => theme.colors.grayscale.base};
   }
 `;
 
-export interface ListViewProps {
-  columns: any[];
-  data: any[];
-  count: number;
-  pageSize: number;
-  fetchData: (conf: FetchDataConfig) => any;
-  loading: boolean;
-  className?: string;
-  initialSort?: SortColumn[];
-  filters?: Filters;
-  bulkActions?: Array<{
-    key: string;
-    name: React.ReactNode;
-    onSelect: (rows: any[]) => any;
-    type?: 'primary' | 'secondary' | 'danger';
-  }>;
-  bulkSelectEnabled?: boolean;
-  disableBulkSelect?: () => void;
-  renderBulkSelectCopy?: (selects: any[]) => React.ReactNode;
-}
-
 const BulkSelectWrapper = styled(Alert)`
   border-radius: 0;
   margin-bottom: 0;
@@ -257,6 +107,89 @@ const bulkSelectColumnConfig = {
   size: 'sm',
 };
 
+const ViewModeContainer = styled.div`
+  padding: ${({ theme }) => theme.gridUnit * 6}px 0px
+    ${({ theme }) => theme.gridUnit * 2}px
+    ${({ theme }) => theme.gridUnit * 4}px;
+  display: inline-block;
+  position: relative;
+  top: 8px;
+
+  .toggle-button {
+    display: inline-block;
+    border-radius: ${({ theme }) => theme.gridUnit / 2}px;
+    padding: ${({ theme }) => theme.gridUnit}px;
+    padding-bottom: 0;
+
+    &:first-of-type {
+      margin-right: ${({ theme }) => theme.gridUnit * 2}px;
+    }
+  }
+
+  .active {
+    background-color: ${({ theme }) => theme.colors.grayscale.base};
+    svg {
+      color: ${({ theme }) => theme.colors.grayscale.light5};
+    }
+  }
+`;
+
+const ViewModeToggle = ({
+  mode,
+  setMode,
+}: {
+  mode: 'table' | 'card';
+  setMode: (mode: 'table' | 'card') => void;
+}) => {
+  return (
+    <ViewModeContainer>
+      <div
+        role="button"
+        tabIndex={0}
+        onClick={e => {
+          e.currentTarget.blur();
+          setMode('card');
+        }}
+        className={cx('toggle-button', { active: mode === 'card' })}
+      >
+        <Icon name="card-view" />
+      </div>
+      <div
+        role="button"
+        tabIndex={0}
+        onClick={e => {
+          e.currentTarget.blur();
+          setMode('table');
+        }}
+        className={cx('toggle-button', { active: mode === 'table' })}
+      >
+        <Icon name="list-view" />
+      </div>
+    </ViewModeContainer>
+  );
+};
+export interface ListViewProps<T = any> {
+  columns: any[];
+  data: T[];
+  count: number;
+  pageSize: number;
+  fetchData: (conf: FetchDataConfig) => any;
+  loading: boolean;
+  className?: string;
+  initialSort?: SortColumn[];
+  filters?: Filters;
+  bulkActions?: Array<{
+    key: string;
+    name: React.ReactNode;
+    onSelect: (rows: any[]) => any;
+    type?: 'primary' | 'secondary' | 'danger';
+  }>;
+  bulkSelectEnabled?: boolean;
+  disableBulkSelect?: () => void;
+  renderBulkSelectCopy?: (selects: any[]) => React.ReactNode;
+  renderCard?: (row: T) => React.ReactNode;
+}
+
 const ListView: FunctionComponent<ListViewProps> = ({
   columns,
   data,
@@ -271,6 +204,7 @@ const ListView: FunctionComponent<ListViewProps> = ({
   bulkSelectEnabled = false,
   disableBulkSelect = () => {},
   renderBulkSelectCopy = selected => t('%s Selected', selected.length),
+  renderCard,
 }) => {
   const {
     getTableProps,
@@ -310,10 +244,18 @@ const ListView: FunctionComponent<ListViewProps> = ({
     });
   }
 
+  const cardViewEnabled = Boolean(renderCard);
+  const [viewingMode, setViewingMode] = useState<'table' | 'card'>(
+    cardViewEnabled ? 'card' : 'table',
+  );
+
   return (
     <ListViewStyles>
       <div className={`superset-list-view ${className}`}>
         <div className="header">
+          {cardViewEnabled && (
+            <ViewModeToggle mode={viewingMode} setMode={setViewingMode} />
+          )}
           {filterable && (
             <FilterControls
               filters={filters}
@@ -364,36 +306,42 @@ const ListView: FunctionComponent<ListViewProps> = ({
               )}
             </BulkSelectWrapper>
           )}
-          <TableCollection
-            getTableProps={getTableProps}
-            getTableBodyProps={getTableBodyProps}
-            prepareRow={prepareRow}
-            headerGroups={headerGroups}
-            rows={rows}
-            loading={loading}
-          />
+          {viewingMode === 'card' && (
+            <CardCollection
+              prepareRow={prepareRow}
+              renderCard={renderCard}
+              rows={rows}
+              loading={loading}
+            />
+          )}
+          {viewingMode === 'table' && (
+            <TableCollection
+              getTableProps={getTableProps}
+              getTableBodyProps={getTableBodyProps}
+              prepareRow={prepareRow}
+              headerGroups={headerGroups}
+              rows={rows}
+              loading={loading}
+            />
+          )}
         </div>
-        <div className="footer">
-          <Row>
-            <Col>
-              <span className="row-count-container">
-                showing{' '}
-                <strong>
-                  {pageSize * pageIndex + (rows.length && 1)}-
-                  {pageSize * pageIndex + rows.length}
-                </strong>{' '}
-                of <strong>{count}</strong>
-              </span>
-            </Col>
-          </Row>
+      </div>
+      <div className="pagination-container">
+        <Pagination
+          totalPages={pageCount || 0}
+          currentPage={pageCount ? pageIndex + 1 : 0}
+          onChange={(p: number) => gotoPage(p - 1)}
+          hideFirstAndLastPageLinks
+        />
+        <div className="row-count-container">
+          {t(
+            '%s-%s of %s',
+            pageSize * pageIndex + (rows.length && 1),
+            pageSize * pageIndex + rows.length,
+            count,
+          )}
         </div>
       </div>
-      <Pagination
-        totalPages={pageCount || 0}
-        currentPage={pageCount ? pageIndex + 1 : 0}
-        onChange={(p: number) => gotoPage(p - 1)}
-        hideFirstAndLastPageLinks
-      />
     </ListViewStyles>
   );
 };
diff --git a/superset-frontend/src/components/ListView/TableCollection.tsx 
b/superset-frontend/src/components/ListView/TableCollection.tsx
index 42bb720..7fd37f3 100644
--- a/superset-frontend/src/components/ListView/TableCollection.tsx
+++ b/superset-frontend/src/components/ListView/TableCollection.tsx
@@ -22,7 +22,7 @@ import { TableInstance } from 'react-table';
 import styled from '@superset-ui/style';
 import Icon from 'src/components/Icon';
 
-interface Props {
+interface TableCollectionProps {
   getTableProps: (userProps?: any) => any;
   getTableBodyProps: (userProps?: any) => any;
   prepareRow: TableInstance['prepareRow'];
@@ -32,7 +32,17 @@ interface Props {
 }
 
 const Table = styled.table`
+  border-collapse: separate;
+
   th {
+    background: ${({ theme }) => theme.colors.grayscale.light5};
+    position: sticky;
+    top: 0;
+
+    &:first-of-type {
+      padding-left: ${({ theme }) => theme.gridUnit * 4}px;
+    }
+
     &.xs {
       min-width: 25px;
     }
@@ -58,6 +68,7 @@ const Table = styled.table`
       position: relative;
     }
   }
+
   td {
     &.xs {
       width: 25px;
@@ -78,6 +89,105 @@ const Table = styled.table`
       width: 200px;
     }
   }
+
+  .table-cell-loader {
+    position: relative;
+
+    .loading-bar {
+      background-color: ${({ theme }) => theme.colors.secondary.light4};
+      border-radius: 7px;
+
+      span {
+        visibility: hidden;
+      }
+    }
+
+    &:after {
+      position: absolute;
+      transform: translateY(-50%);
+      top: 50%;
+      left: 0;
+      content: '';
+      display: block;
+      width: 100%;
+      height: 48px;
+      background-image: linear-gradient(
+        100deg,
+        rgba(255, 255, 255, 0),
+        rgba(255, 255, 255, 0.5) 60%,
+        rgba(255, 255, 255, 0) 80%
+      );
+      background-size: 200px 48px;
+      background-position: -100px 0;
+      background-repeat: no-repeat;
+      animation: loading-shimmer 1s infinite;
+    }
+  }
+
+  .actions {
+    white-space: nowrap;
+    min-width: 100px;
+
+    svg,
+    i {
+      margin-right: 8px;
+
+      &:hover {
+        path {
+          fill: ${({ theme }) => theme.colors.primary.base};
+        }
+      }
+    }
+  }
+
+  .table-row {
+    .actions {
+      opacity: 0;
+    }
+
+    &:hover {
+      background-color: ${({ theme }) => theme.colors.secondary.light5};
+
+      .actions {
+        opacity: 1;
+        transition: opacity ease-in ${({ theme }) => theme.transitionTiming}s;
+      }
+    }
+  }
+
+  .table-row-selected {
+    background-color: ${({ theme }) => theme.colors.secondary.light4};
+
+    &:hover {
+      background-color: ${({ theme }) => theme.colors.secondary.light4};
+    }
+  }
+
+  .table-cell {
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+    max-width: 300px;
+    line-height: 1;
+    vertical-align: middle;
+    &:first-of-type {
+      padding-left: ${({ theme }) => theme.gridUnit * 4}px;
+    }
+  }
+
+  .sort-icon {
+    position: absolute;
+  }
+
+  @keyframes loading-shimmer {
+    40% {
+      background-position: 100% 0;
+    }
+
+    100% {
+      background-position: 100% 0;
+    }
+  }
 `;
 
 export default function TableCollection({
@@ -87,7 +197,7 @@ export default function TableCollection({
   headerGroups,
   rows,
   loading,
-}: Props) {
+}: TableCollectionProps) {
   return (
     <Table {...getTableProps()} className="table table-hover">
       <thead>
diff --git a/superset-frontend/src/types/Chart.ts 
b/superset-frontend/src/components/ListView/index.ts
similarity index 75%
copy from superset-frontend/src/types/Chart.ts
copy to superset-frontend/src/components/ListView/index.ts
index 9b74337..30be8a1 100644
--- a/superset-frontend/src/types/Chart.ts
+++ b/superset-frontend/src/components/ListView/index.ts
@@ -17,17 +17,7 @@
  * under the License.
  */
 
-/**
- * The Chart model as returned from the API
- */
+export * from './ListView';
+export * from './types';
 
-export default interface Chart {
-  id: number;
-  url: string;
-  viz_type: string;
-  slice_name: string;
-  creator: string;
-  changed_on: string;
-  description: string | null;
-  cache_timeout: number | null;
-}
+export { default } from './ListView';
diff --git 
a/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx 
b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx
new file mode 100644
index 0000000..07881f6
--- /dev/null
+++ b/superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx
@@ -0,0 +1,75 @@
+/**
+ * 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 { action } from '@storybook/addon-actions';
+import { withKnobs, boolean } from '@storybook/addon-knobs';
+import DashboardImg from 'images/dashboard-card-fallback.png';
+import ChartImg from 'images/chart-card-fallback.png';
+import { Dropdown, Menu } from 'src/common/components';
+import Icon from 'src/components/Icon';
+import FaveStar from 'src/components/FaveStar';
+import ListViewCard from './';
+
+export default {
+  title: 'ListViewCard',
+  component: ListViewCard,
+  decorators: [withKnobs],
+};
+
+export const SupersetListViewCard = () => {
+  return (
+    <ListViewCard
+      title="Superset Card Title"
+      url="/superset/dashboard/births/"
+      imgURL={DashboardImg}
+      imgFallbackURL={ChartImg}
+      description="Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
+      coverLeft="Left Section"
+      coverRight="Right Section"
+      actions={
+        <ListViewCard.Actions>
+          <FaveStar
+            itemId={0}
+            fetchFaveStar={action('fetchFaveStar')}
+            saveFaveStar={action('saveFaveStar')}
+            isStarred={boolean('isStarred', false)}
+          />
+          <Dropdown
+            overlay={
+              <Menu>
+                <Menu.Item
+                  role="button"
+                  tabIndex={0}
+                  onClick={action('Delete')}
+                >
+                  <ListViewCard.MenuIcon name="trash" /> Delete
+                </Menu.Item>
+                <Menu.Item role="button" tabIndex={0} onClick={action('Edit')}>
+                  <ListViewCard.MenuIcon name="pencil" /> Edit
+                </Menu.Item>
+              </Menu>
+            }
+          >
+            <Icon name="more" />
+          </Dropdown>
+        </ListViewCard.Actions>
+      }
+    />
+  );
+};
diff --git a/superset-frontend/src/components/ListViewCard/index.tsx 
b/superset-frontend/src/components/ListViewCard/index.tsx
new file mode 100644
index 0000000..25e731d
--- /dev/null
+++ b/superset-frontend/src/components/ListViewCard/index.tsx
@@ -0,0 +1,197 @@
+/**
+ * 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 styled from '@superset-ui/style';
+import Icon from 'src/components/Icon';
+import { Card } from 'src/common/components';
+
+const MenuIcon = styled(Icon)`
+  width: ${({ theme }) => theme.gridUnit * 4}px;
+  height: ${({ theme }) => theme.gridUnit * 4}px;
+  position: relative;
+  top: ${({ theme }) => theme.gridUnit / 2}px;
+`;
+
+const ActionsWrapper = styled.div`
+  width: 64px;
+  display: flex;
+  justify-content: space-between;
+`;
+
+const StyledCard = styled(Card)`
+  width: 459px;
+
+  .ant-card-body {
+    padding: ${({ theme }) => theme.gridUnit * 4}px
+      ${({ theme }) => theme.gridUnit * 2}px;
+  }
+  .ant-card-meta-detail > div:not(:last-child) {
+    margin-bottom: 0;
+  }
+`;
+
+const Cover = styled.div`
+  height: 264px;
+  overflow: hidden;
+
+  .cover-footer {
+    transform: translateY(${({ theme }) => theme.gridUnit * 9}px);
+    transition: ${({ theme }) => theme.transitionTiming}s ease-out;
+  }
+
+  &:hover {
+    .cover-footer {
+      transform: translateY(0);
+    }
+  }
+`;
+
+const GradientContainer = styled.div`
+  position: relative;
+  display: inline-block;
+
+  &:after {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    display: inline-block;
+    background: linear-gradient(
+      180deg,
+      rgba(0, 0, 0, 0) 47.83%,
+      rgba(0, 0, 0, 0.219135) 79.64%,
+      rgba(0, 0, 0, 0.5) 100%
+    );
+  }
+`;
+const CardCoverImg = styled.img`
+  display: block;
+  object-fit: cover;
+  width: 459px;
+  height: 264px;
+`;
+
+const TitleContainer = styled.div`
+  display: flex;
+  justify-content: flex-start;
+  flex-direction: row;
+
+  .card-actions {
+    margin-left: auto;
+    align-self: flex-end;
+    padding-left: ${({ theme }) => theme.gridUnit * 8}px;
+  }
+`;
+
+const TitleLink = styled.a`
+  color: ${({ theme }) => theme.colors.grayscale.dark1} !important;
+  overflow: hidden;
+  text-overflow: ellipsis;
+
+  & + .title-right {
+    margin-left: ${({ theme }) => theme.gridUnit * 2}px;
+  }
+`;
+
+const CoverFooter = styled.div`
+  display: flex;
+  flex-wrap: nowrap;
+  position: relative;
+  top: -${({ theme }) => theme.gridUnit * 9}px;
+  padding: 0 8px;
+`;
+
+const CoverFooterLeft = styled.div`
+  flex: 1;
+  overflow: hidden;
+`;
+
+const CoverFooterRight = styled.div`
+  align-self: flex-end;
+  margin-left: auto;
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+`;
+
+interface CardProps {
+  title: React.ReactNode;
+  url: string;
+  imgURL: string;
+  imgFallbackURL: string;
+  description: string;
+  titleRight?: React.ReactNode;
+  coverLeft?: React.ReactNode;
+  coverRight?: React.ReactNode;
+  actions: React.ReactNode;
+}
+
+function ListViewCard({
+  title,
+  url,
+  titleRight,
+  imgURL,
+  imgFallbackURL,
+  description,
+  coverLeft,
+  coverRight,
+  actions,
+}: CardProps) {
+  return (
+    <StyledCard
+      cover={
+        <Cover>
+          <a href={url}>
+            <GradientContainer>
+              <CardCoverImg
+                src={imgURL}
+                onError={e => {
+                  e.currentTarget.src = imgFallbackURL;
+                }}
+              />
+            </GradientContainer>
+          </a>
+          <CoverFooter className="cover-footer">
+            {coverLeft && <CoverFooterLeft>{coverLeft}</CoverFooterLeft>}
+            {coverRight && <CoverFooterRight>{coverRight}</CoverFooterRight>}
+          </CoverFooter>
+        </Cover>
+      }
+    >
+      <Card.Meta
+        title={
+          <>
+            <TitleContainer>
+              <TitleLink href={url}>{title}</TitleLink>
+              {titleRight && <div className="title-right"> {titleRight}</div>}
+              <div className="card-actions">{actions}</div>
+            </TitleContainer>
+          </>
+        }
+        description={description}
+      />
+    </StyledCard>
+  );
+}
+
+ListViewCard.Actions = ActionsWrapper;
+ListViewCard.MenuIcon = MenuIcon;
+export default ListViewCard;
diff --git a/superset-frontend/src/components/Pagination.tsx 
b/superset-frontend/src/components/Pagination.tsx
index a023f09..78e806c 100644
--- a/superset-frontend/src/components/Pagination.tsx
+++ b/superset-frontend/src/components/Pagination.tsx
@@ -77,6 +77,7 @@ interface PaginationProps {
 const PaginationList = styled.ul`
   display: inline-block;
   margin: 16px 0;
+  padding: 0;
 
   li {
     display: inline;
diff --git a/superset-frontend/src/components/SearchInput.tsx 
b/superset-frontend/src/components/SearchInput.tsx
index 670de69..314c9a4 100644
--- a/superset-frontend/src/components/SearchInput.tsx
+++ b/superset-frontend/src/components/SearchInput.tsx
@@ -49,20 +49,18 @@ const commonStyles = `
   position: absolute;
   z-index: 2;
   display: block;
-  width: 28px;
-  height: 28px;
   cursor: pointer;
 `;
 const SearchIcon = styled(Icon)`
   ${commonStyles}
-  top: 2px;
+  top: 1px;
   left: 2px;
 `;
 
 const ClearIcon = styled(Icon)`
   ${commonStyles}
   right: 0px;
-  top: 3px;
+  top: 1px;
 `;
 
 export default function SearchInput({
diff --git a/superset-frontend/src/explore/components/PropertiesModal.tsx 
b/superset-frontend/src/explore/components/PropertiesModal.tsx
index 0cb5988..bbcb508 100644
--- a/superset-frontend/src/explore/components/PropertiesModal.tsx
+++ b/superset-frontend/src/explore/components/PropertiesModal.tsx
@@ -38,6 +38,7 @@ import FormLabel from 'src/components/FormLabel';
 import getClientErrorObject from '../../utils/getClientErrorObject';
 
 export type Slice = {
+  id?: number;
   slice_id: number;
   slice_name: string;
   description: string | null;
diff --git a/superset-frontend/src/types/Chart.ts 
b/superset-frontend/src/types/Chart.ts
index 9b74337..e78d810 100644
--- a/superset-frontend/src/types/Chart.ts
+++ b/superset-frontend/src/types/Chart.ts
@@ -21,6 +21,8 @@
  * The Chart model as returned from the API
  */
 
+import Owner from './Owner';
+
 export default interface Chart {
   id: number;
   url: string;
@@ -30,4 +32,8 @@ export default interface Chart {
   changed_on: string;
   description: string | null;
   cache_timeout: number | null;
+  thumbnail_url?: string;
+  changed_on_delta_humanized?: string;
+  owners?: Owner[];
+  datasource_name_text?: string;
 }
diff --git a/superset-frontend/src/types/Chart.ts 
b/superset-frontend/src/types/Owner.ts
similarity index 76%
copy from superset-frontend/src/types/Chart.ts
copy to superset-frontend/src/types/Owner.ts
index 9b74337..890115e 100644
--- a/superset-frontend/src/types/Chart.ts
+++ b/superset-frontend/src/types/Owner.ts
@@ -18,16 +18,12 @@
  */
 
 /**
- * The Chart model as returned from the API
+ * The Owner model as returned from the API
  */
 
-export default interface Chart {
-  id: number;
-  url: string;
-  viz_type: string;
-  slice_name: string;
-  creator: string;
-  changed_on: string;
-  description: string | null;
-  cache_timeout: number | null;
+export default interface Owner {
+  first_name: string;
+  id: string;
+  last_name: string;
+  username: string;
 }
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx 
b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 3a651d1..785a238 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -30,17 +30,21 @@ import {
 } from 'src/views/CRUD/utils';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu from 'src/components/Menu/SubMenu';
+import AvatarIcon from 'src/components/AvatarIcon';
 import Icon from 'src/components/Icon';
 import FaveStar from 'src/components/FaveStar';
-import ListView, { ListViewProps } from 'src/components/ListView/ListView';
-import {
+import ListView, {
+  ListViewProps,
   FetchDataConfig,
   Filters,
   SelectOption,
-} from 'src/components/ListView/types';
+} from 'src/components/ListView';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import PropertiesModal, { Slice } from 
'src/explore/components/PropertiesModal';
 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';
 
 const PAGE_SIZE = 25;
 const FAVESTAR_BASE_URL = '/superset/favstar/slice';
@@ -53,7 +57,7 @@ interface Props {
 interface State {
   bulkSelectEnabled: boolean;
   chartCount: number;
-  charts: any[];
+  charts: Chart[];
   favoriteStatus: object;
   lastFetchDataConfig: FetchDataConfig | null;
   loading: boolean;
@@ -191,7 +195,7 @@ class ChartList extends React.PureComponent<Props, State> {
         },
       }: any) => <a href={dsUrl}>{dsNameTxt}</a>,
       Header: t('Datasource'),
-      accessor: 'datasource_id',
+      accessor: 'datasource_name',
     },
     {
       Cell: ({
@@ -225,7 +229,7 @@ class ChartList extends React.PureComponent<Props, State> {
       disableSortBy: true,
     },
     {
-      accessor: 'datasource',
+      accessor: 'datasource_id',
       hidden: true,
       disableSortBy: true,
     },
@@ -457,6 +461,85 @@ class ChartList extends React.PureComponent<Props, State> {
       });
   };
 
+  renderCard = (props: Chart) => {
+    const menu = (
+      <Menu>
+        {this.canDelete && (
+          <Menu.Item>
+            <ConfirmStatusChange
+              title={t('Please Confirm')}
+              description={
+                <>
+                  {t('Are you sure you want to delete')}{' '}
+                  <b>{props.slice_name}</b>?
+                </>
+              }
+              onConfirm={() => this.handleChartDelete(props)}
+            >
+              {confirmDelete => (
+                <div
+                  role="button"
+                  tabIndex={0}
+                  className="action-button"
+                  onClick={confirmDelete}
+                >
+                  <ListViewCard.MenuIcon name="trash" /> Delete
+                </div>
+              )}
+            </ConfirmStatusChange>
+          </Menu.Item>
+        )}
+        {this.canEdit && (
+          <Menu.Item
+            role="button"
+            tabIndex={0}
+            onClick={() => this.openChartEditModal(props)}
+          >
+            <ListViewCard.MenuIcon name="pencil" /> Edit
+          </Menu.Item>
+        )}
+      </Menu>
+    );
+
+    return (
+      <ListViewCard
+        title={props.slice_name}
+        url={props.url}
+        imgURL={props.thumbnail_url ?? ''}
+        imgFallbackURL={'/static/assets/images/chart-card-fallback.png'}
+        description={t('Last modified %s', props.changed_on_delta_humanized)}
+        coverLeft={(props.owners || []).slice(0, 5).map(owner => (
+          <AvatarIcon
+            key={owner.id}
+            uniqueKey={`${owner.username}-${props.id}`}
+            firstName={owner.first_name}
+            lastName={owner.last_name}
+            iconSize={24}
+            textSize={9}
+          />
+        ))}
+        coverRight={
+          <Label 
className="secondaryLabel">{props.datasource_name_text}</Label>
+        }
+        actions={
+          <ListViewCard.Actions>
+            <FaveStar
+              itemId={props.id}
+              fetchFaveStar={this.fetchMethods.fetchFaveStar}
+              saveFaveStar={this.fetchMethods.saveFaveStar}
+              isStarred={!!this.state.favoriteStatus[props.id]}
+              width={20}
+              height={20}
+            />
+            <Dropdown overlay={menu}>
+              <Icon name="more" />
+            </Dropdown>
+          </ListViewCard.Actions>
+        }
+      />
+    );
+  };
+
   render() {
     const {
       bulkSelectEnabled,
@@ -519,6 +602,7 @@ class ChartList extends React.PureComponent<Props, State> {
                 initialSort={this.initialSort}
                 loading={loading}
                 pageSize={PAGE_SIZE}
+                renderCard={this.renderCard}
               />
             );
           }}
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx 
b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index f91fb05..dc4194e 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -28,13 +28,21 @@ import {
 } from 'src/views/CRUD/utils';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import SubMenu from 'src/components/Menu/SubMenu';
-import ListView, { ListViewProps } from 'src/components/ListView/ListView';
+import AvatarIcon from 'src/components/AvatarIcon';
+import ListView, {
+  ListViewProps,
+  FetchDataConfig,
+  Filters,
+} from 'src/components/ListView';
 import ExpandableList from 'src/components/ExpandableList';
-import { FetchDataConfig, Filters } from 'src/components/ListView/types';
+import Owner from 'src/types/Owner';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 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';
 
 const PAGE_SIZE = 25;
 const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
@@ -47,7 +55,7 @@ interface Props {
 interface State {
   bulkSelectEnabled: boolean;
   dashboardCount: number;
-  dashboards: any[];
+  dashboards: Dashboard[];
   favoriteStatus: object;
   dashboardToEdit: Dashboard | null;
   lastFetchDataConfig: FetchDataConfig | null;
@@ -64,6 +72,8 @@ interface Dashboard {
   id: number;
   published: boolean;
   url: string;
+  thumbnail_url: string;
+  owners: Owner[];
 }
 
 class DashboardList extends React.PureComponent<Props, State> {
@@ -205,61 +215,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
       disableSortBy: true,
     },
     {
-      Cell: ({ row: { original } }: any) => {
-        const handleDelete = () => this.handleDashboardDelete(original);
-        const handleEdit = () => this.openDashboardEditModal(original);
-        const handleExport = () => this.handleBulkDashboardExport([original]);
-        if (!this.canEdit && !this.canDelete && !this.canExport) {
-          return null;
-        }
-        return (
-          <span className="actions">
-            {this.canDelete && (
-              <ConfirmStatusChange
-                title={t('Please Confirm')}
-                description={
-                  <>
-                    {t('Are you sure you want to delete')}{' '}
-                    <b>{original.dashboard_title}</b>?
-                  </>
-                }
-                onConfirm={handleDelete}
-              >
-                {confirmDelete => (
-                  <span
-                    role="button"
-                    tabIndex={0}
-                    className="action-button"
-                    onClick={confirmDelete}
-                  >
-                    <Icon name="trash" />
-                  </span>
-                )}
-              </ConfirmStatusChange>
-            )}
-            {this.canExport && (
-              <span
-                role="button"
-                tabIndex={0}
-                className="action-button"
-                onClick={handleExport}
-              >
-                <Icon name="share" />
-              </span>
-            )}
-            {this.canEdit && (
-              <span
-                role="button"
-                tabIndex={0}
-                className="action-button"
-                onClick={handleEdit}
-              >
-                <Icon name="pencil" />
-              </span>
-            )}
-          </span>
-        );
-      },
+      Cell: ({ row: { original } }: any) => this.renderActions(original),
       Header: t('Actions'),
       id: 'actions',
       disableSortBy: true,
@@ -444,6 +400,148 @@ class DashboardList extends React.PureComponent<Props, 
State> {
       });
   };
 
+  renderActions(original: Dashboard) {
+    const handleDelete = () => this.handleDashboardDelete(original);
+    const handleEdit = () => this.openDashboardEditModal(original);
+    const handleExport = () => this.handleBulkDashboardExport([original]);
+    if (!this.canEdit && !this.canDelete && !this.canExport) {
+      return null;
+    }
+    return (
+      <span className="actions">
+        {this.canDelete && (
+          <ConfirmStatusChange
+            title={t('Please Confirm')}
+            description={
+              <>
+                {t('Are you sure you want to delete')}{' '}
+                <b>{original.dashboard_title}</b>?
+              </>
+            }
+            onConfirm={handleDelete}
+          >
+            {confirmDelete => (
+              <span
+                role="button"
+                tabIndex={0}
+                className="action-button"
+                onClick={confirmDelete}
+              >
+                <Icon name="trash" />
+              </span>
+            )}
+          </ConfirmStatusChange>
+        )}
+        {this.canExport && (
+          <span
+            role="button"
+            tabIndex={0}
+            className="action-button"
+            onClick={handleExport}
+          >
+            <Icon name="share" />
+          </span>
+        )}
+        {this.canEdit && (
+          <span
+            role="button"
+            tabIndex={0}
+            className="action-button"
+            onClick={handleEdit}
+          >
+            <Icon name="pencil" />
+          </span>
+        )}
+      </span>
+    );
+  }
+
+  renderCard = (props: Dashboard) => {
+    const menu = (
+      <Menu>
+        {this.canDelete && (
+          <Menu.Item>
+            <ConfirmStatusChange
+              title={t('Please Confirm')}
+              description={
+                <>
+                  {t('Are you sure you want to delete')}{' '}
+                  <b>{props.dashboard_title}</b>?
+                </>
+              }
+              onConfirm={() => this.handleDashboardDelete(props)}
+            >
+              {confirmDelete => (
+                <div
+                  role="button"
+                  tabIndex={0}
+                  className="action-button"
+                  onClick={confirmDelete}
+                >
+                  <ListViewCard.MenuIcon name="trash" /> Delete
+                </div>
+              )}
+            </ConfirmStatusChange>
+          </Menu.Item>
+        )}
+        {this.canExport && (
+          <Menu.Item
+            role="button"
+            tabIndex={0}
+            onClick={() => this.handleBulkDashboardExport([props])}
+          >
+            <ListViewCard.MenuIcon name="share" /> Export
+          </Menu.Item>
+        )}
+        {this.canEdit && (
+          <Menu.Item
+            role="button"
+            tabIndex={0}
+            onClick={() => this.openDashboardEditModal(props)}
+          >
+            <ListViewCard.MenuIcon name="pencil" /> Edit
+          </Menu.Item>
+        )}
+      </Menu>
+    );
+
+    return (
+      <ListViewCard
+        title={props.dashboard_title}
+        titleRight={<Label>{props.published ? 'published' : 'draft'}</Label>}
+        url={props.url}
+        imgURL={props.thumbnail_url}
+        imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
+        description={t('Last modified %s', props.changed_on_delta_humanized)}
+        coverLeft={props.owners.slice(0, 5).map(owner => (
+          <AvatarIcon
+            key={owner.id}
+            uniqueKey={`${owner.username}-${props.id}`}
+            firstName={owner.first_name}
+            lastName={owner.last_name}
+            iconSize={24}
+            textSize={9}
+          />
+        ))}
+        actions={
+          <ListViewCard.Actions>
+            <FaveStar
+              itemId={props.id}
+              fetchFaveStar={this.fetchMethods.fetchFaveStar}
+              saveFaveStar={this.fetchMethods.saveFaveStar}
+              isStarred={!!this.state.favoriteStatus[props.id]}
+              width={20}
+              height={20}
+            />
+            <Dropdown overlay={menu}>
+              <Icon name="more" />
+            </Dropdown>
+          </ListViewCard.Actions>
+        }
+      />
+    );
+  };
+
   render() {
     const {
       bulkSelectEnabled,
@@ -495,6 +593,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
                 {dashboardToEdit && (
                   <PropertiesModal
                     dashboardId={dashboardToEdit.id}
+                    show
                     onHide={() => this.setState({ dashboardToEdit: null })}
                     onSubmit={this.handleDashboardEdit}
                   />
@@ -512,6 +611,7 @@ class DashboardList extends React.PureComponent<Props, 
State> {
                   initialSort={this.initialSort}
                   loading={loading}
                   pageSize={PAGE_SIZE}
+                  renderCard={this.renderCard}
                 />
               </>
             );
diff --git a/superset-frontend/src/views/CRUD/dataset/DatasetList.tsx 
b/superset-frontend/src/views/CRUD/dataset/DatasetList.tsx
index a9bebd7..8ee1e5f 100644
--- a/superset-frontend/src/views/CRUD/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/dataset/DatasetList.tsx
@@ -29,10 +29,14 @@ import { createFetchRelated, createErrorHandler } from 
'src/views/CRUD/utils';
 import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
 import DatasourceModal from 'src/datasource/DatasourceModal';
 import DeleteModal from 'src/components/DeleteModal';
-import ListView, { ListViewProps } from 'src/components/ListView/ListView';
+import ListView, {
+  ListViewProps,
+  FetchDataConfig,
+  Filters,
+} from 'src/components/ListView';
 import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
 import AvatarIcon from 'src/components/AvatarIcon';
-import { FetchDataConfig, Filters } from 'src/components/ListView/types';
+import Owner from 'src/types/Owner';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import TooltipWrapper from 'src/components/TooltipWrapper';
 import Icon from 'src/components/Icon';
@@ -40,13 +44,6 @@ import AddDatasetModal from './AddDatasetModal';
 
 const PAGE_SIZE = 25;
 
-type Owner = {
-  first_name: string;
-  id: string;
-  last_name: string;
-  username: string;
-};
-
 type Dataset = {
   changed_by_name: string;
   changed_by_url: string;
@@ -359,10 +356,9 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
           .map((owner: Owner) => (
             <AvatarIcon
               key={owner.id}
-              tableName={tableName}
+              uniqueKey={`${tableName}-${owner.username}`}
               firstName={owner.first_name}
               lastName={owner.last_name}
-              userName={owner.username}
               iconSize={24}
               textSize={9}
             />
@@ -379,7 +375,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
       disableSortBy: true,
     },
     {
-      Cell: ({ row: { state, original } }: any) => {
+      Cell: ({ row: { original } }: any) => {
         const handleEdit = () => openDatasetEditModal(original);
         const handleDelete = () => openDatasetDeleteModal(original);
         if (!canEdit && !canDelete) {
diff --git a/superset-frontend/src/welcome/DashboardTable.tsx 
b/superset-frontend/src/welcome/DashboardTable.tsx
index 971b54f..345f8e8 100644
--- a/superset-frontend/src/welcome/DashboardTable.tsx
+++ b/superset-frontend/src/welcome/DashboardTable.tsx
@@ -20,10 +20,9 @@ import React from 'react';
 import { t } from '@superset-ui/translation';
 import { SupersetClient } from '@superset-ui/connection';
 import { debounce } from 'lodash';
-import ListView from 'src/components/ListView/ListView';
+import ListView, { FetchDataConfig } from 'src/components/ListView';
 import withToasts from 'src/messageToasts/enhancers/withToasts';
 import { Dashboard } from 'src/types/bootstrapTypes';
-import { FetchDataConfig } from 'src/components/ListView/types';
 
 const PAGE_SIZE = 25;
 
diff --git a/superset/charts/api.py b/superset/charts/api.py
index 6c3792e..09f4e06 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -116,15 +116,21 @@ class ChartRestApi(BaseSupersetModelRestApi):
         "datasource_url",
         "table.default_endpoint",
         "table.table_name",
+        "thumbnail_url",
         "viz_type",
         "params",
         "cache_timeout",
+        "owners.id",
+        "owners.username",
+        "owners.first_name",
+        "owners.last_name",
     ]
     list_select_columns = list_columns + ["changed_on", "changed_by_fk"]
     order_columns = [
         "slice_name",
         "viz_type",
         "datasource_name",
+        "datasource_id",
         "changed_by.first_name",
         "changed_on_delta_humanized",
     ]

Reply via email to