From db88cec431bca04608c6580192714da03bed1e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CA=88=E1=B5=83=E1=B5=A2?= Date: Thu, 13 Aug 2020 14:46:56 -0700 Subject: [PATCH] feat: SIP-34 card/grid views for dashboards and charts (#10526) --- superset-frontend/.storybook/main.js | 2 +- .../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 +- .../CRUD/dashboard/DashboardList_spec.jsx | 13 +- .../views/CRUD/dataset/DatasetList_spec.jsx | 2 +- .../welcome/DashboardTable_spec.tsx | 2 +- .../src/components/AvatarIcon.tsx | 14 +- superset-frontend/src/components/FaveStar.tsx | 2 +- .../src/components/Icon/index.tsx | 39 ++- .../src/components/Label/index.tsx | 7 + .../components/ListView/CardCollection.tsx | 56 +++ .../src/components/ListView/Filters.tsx | 5 +- .../src/components/ListView/ListView.tsx | 326 ++++++++---------- .../components/ListView/TableCollection.tsx | 114 +++++- .../src/components/ListView/index.ts | 23 ++ .../ListViewCard/ListViewCard.stories.tsx | 75 ++++ .../src/components/ListViewCard/index.tsx | 197 +++++++++++ .../src/components/Pagination.tsx | 1 + .../src/components/SearchInput.tsx | 6 +- .../explore/components/PropertiesModal.tsx | 1 + superset-frontend/src/types/Chart.ts | 6 + superset-frontend/src/types/Owner.ts | 29 ++ .../src/views/CRUD/chart/ChartList.tsx | 96 +++++- .../views/CRUD/dashboard/DashboardList.tsx | 216 ++++++++---- .../src/views/CRUD/dataset/DatasetList.tsx | 20 +- .../src/welcome/DashboardTable.tsx | 3 +- superset/charts/api.py | 6 + 31 files changed, 1019 insertions(+), 305 deletions(-) create mode 100644 superset-frontend/images/chart-card-fallback.png create mode 100644 superset-frontend/images/dashboard-card-fallback.png create mode 100644 superset-frontend/images/icons/card-view.svg create mode 100644 superset-frontend/images/icons/list-view.svg create mode 100644 superset-frontend/src/components/ListView/CardCollection.tsx create mode 100644 superset-frontend/src/components/ListView/index.ts create mode 100644 superset-frontend/src/components/ListViewCard/ListViewCard.stories.tsx create mode 100644 superset-frontend/src/components/ListViewCard/index.tsx create mode 100644 superset-frontend/src/types/Owner.ts diff --git a/superset-frontend/.storybook/main.js b/superset-frontend/.storybook/main.js index 2b94128e5cccb..49921cfc59973 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 0000000000000000000000000000000000000000..aa34d4f0136d52dd767e21bd179f961d5f58b024 GIT binary patch literal 3183 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 literal 0 HcmV?d00001 diff --git a/superset-frontend/images/icons/card-view.svg b/superset-frontend/images/icons/card-view.svg new file mode 100644 index 0000000000000..009409b59491e --- /dev/null +++ b/superset-frontend/images/icons/card-view.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/images/icons/list-view.svg b/superset-frontend/images/icons/list-view.svg new file mode 100644 index 0000000000000..9d33b74157b4f --- /dev/null +++ b/superset-frontend/images/icons/list-view.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/images/icons/search.svg b/superset-frontend/images/icons/search.svg index 5b86f13859f66..bef0709fd65b4 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. --> - + Icon / Search@1.5x Created with Sketch. 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 cab27f7d4223b..2083b4a6bec14 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 5c448ea59d44b..eef4ca03eb45b 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 01fb2e80c9e41..53de35d0e7a3f 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 d8f4f2b0346a0..b612fbbb68a30 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 7dcf8f7eff431..f1a2fd32ab25d 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} > - { } viewBox="0 0 16 15" width={this.props.width || 20} - height="auto" + height={this.props.height || 'auto'} /> diff --git a/superset-frontend/src/components/Icon/index.tsx b/superset-frontend/src/components/Icon/index.tsx index a0d516db1c189..c325c8133f862 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> > = { '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 { 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 ; + return ( + + ); }; export default Icon; diff --git a/superset-frontend/src/components/Label/index.tsx b/superset-frontend/src/components/Label/index.tsx index 359b95e5f5ce9..58c336e1884d8 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 0000000000000..6668850369c5f --- /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 ( + + {rows.map(row => { + if (!renderCard) return null; + prepareRow(row); + return ( +
{renderCard({ ...row.original, loading })}
+ ); + })} + + ); +} diff --git a/superset-frontend/src/components/ListView/Filters.tsx b/superset-frontend/src/components/ListView/Filters.tsx index c6017687ed31f..a27a1d6baa623 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 2f797e975a24b..1a8313b684692 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 ( + +
{ + e.currentTarget.blur(); + setMode('card'); + }} + className={cx('toggle-button', { active: mode === 'card' })} + > + +
+
{ + e.currentTarget.blur(); + setMode('table'); + }} + className={cx('toggle-button', { active: mode === 'table' })} + > + +
+
+ ); +}; +export interface ListViewProps { + 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 = ({ columns, data, @@ -271,6 +204,7 @@ const ListView: FunctionComponent = ({ bulkSelectEnabled = false, disableBulkSelect = () => {}, renderBulkSelectCopy = selected => t('%s Selected', selected.length), + renderCard, }) => { const { getTableProps, @@ -310,10 +244,18 @@ const ListView: FunctionComponent = ({ }); } + const cardViewEnabled = Boolean(renderCard); + const [viewingMode, setViewingMode] = useState<'table' | 'card'>( + cardViewEnabled ? 'card' : 'table', + ); + return (
+ {cardViewEnabled && ( + + )} {filterable && ( = ({ )} )} - + {viewingMode === 'card' && ( + + )} + {viewingMode === 'table' && ( + + )}
-
- - - - showing{' '} - - {pageSize * pageIndex + (rows.length && 1)}- - {pageSize * pageIndex + rows.length} - {' '} - of {count} - - - +
+
+ gotoPage(p - 1)} + hideFirstAndLastPageLinks + /> +
+ {t( + '%s-%s of %s', + pageSize * pageIndex + (rows.length && 1), + pageSize * pageIndex + rows.length, + count, + )}
- gotoPage(p - 1)} - hideFirstAndLastPageLinks - /> ); }; diff --git a/superset-frontend/src/components/ListView/TableCollection.tsx b/superset-frontend/src/components/ListView/TableCollection.tsx index 42bb720cf04a4..7fd37f303202c 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 ( diff --git a/superset-frontend/src/components/ListView/index.ts b/superset-frontend/src/components/ListView/index.ts new file mode 100644 index 0000000000000..30be8a1c81011 --- /dev/null +++ b/superset-frontend/src/components/ListView/index.ts @@ -0,0 +1,23 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './ListView'; +export * from './types'; + +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 0000000000000..07881f6bb894d --- /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 ( + + + + + Delete + + + Edit + + + } + > + + + + } + /> + ); +}; diff --git a/superset-frontend/src/components/ListViewCard/index.tsx b/superset-frontend/src/components/ListViewCard/index.tsx new file mode 100644 index 0000000000000..25e731ddeffbf --- /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 ( + + + + { + e.currentTarget.src = imgFallbackURL; + }} + /> + + + + {coverLeft && {coverLeft}} + {coverRight && {coverRight}} + + + } + > + + + {title} + {titleRight &&
{titleRight}
} +
{actions}
+
+ + } + description={description} + /> +
+ ); +} + +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 a023f09198075..78e806c54dead 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 670de69ea895a..314c9a41019f4 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 0cb598888516c..bbcb508e8333e 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 9b74337515e3f..e78d81007b3c4 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/Owner.ts b/superset-frontend/src/types/Owner.ts new file mode 100644 index 0000000000000..890115e9d4e8e --- /dev/null +++ b/superset-frontend/src/types/Owner.ts @@ -0,0 +1,29 @@ +/** + * 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. + */ + +/** + * The Owner model as returned from the API + */ + +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 3a651d17e02b1..785a238bd8e30 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 { }, }: any) => {dsNameTxt}, Header: t('Datasource'), - accessor: 'datasource_id', + accessor: 'datasource_name', }, { Cell: ({ @@ -225,7 +229,7 @@ class ChartList extends React.PureComponent { disableSortBy: true, }, { - accessor: 'datasource', + accessor: 'datasource_id', hidden: true, disableSortBy: true, }, @@ -457,6 +461,85 @@ class ChartList extends React.PureComponent { }); }; + renderCard = (props: Chart) => { + const menu = ( + + {this.canDelete && ( + + + {t('Are you sure you want to delete')}{' '} + {props.slice_name}? + + } + onConfirm={() => this.handleChartDelete(props)} + > + {confirmDelete => ( +
+ Delete +
+ )} +
+
+ )} + {this.canEdit && ( + this.openChartEditModal(props)} + > + Edit + + )} +
+ ); + + return ( + ( + + ))} + coverRight={ + + } + actions={ + + + + + + + } + /> + ); + }; + render() { const { bulkSelectEnabled, @@ -519,6 +602,7 @@ class ChartList extends React.PureComponent { 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 f91fb0561b2cd..dc4194e1c2f3b 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 { @@ -205,61 +215,7 @@ class DashboardList extends React.PureComponent { 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 ( - - {this.canDelete && ( - - {t('Are you sure you want to delete')}{' '} - {original.dashboard_title}? - - } - onConfirm={handleDelete} - > - {confirmDelete => ( - - - - )} - - )} - {this.canExport && ( - - - - )} - {this.canEdit && ( - - - - )} - - ); - }, + Cell: ({ row: { original } }: any) => this.renderActions(original), Header: t('Actions'), id: 'actions', disableSortBy: true, @@ -444,6 +400,148 @@ class DashboardList extends React.PureComponent { }); }; + 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 ( + + {this.canDelete && ( + + {t('Are you sure you want to delete')}{' '} + {original.dashboard_title}? + + } + onConfirm={handleDelete} + > + {confirmDelete => ( + + + + )} + + )} + {this.canExport && ( + + + + )} + {this.canEdit && ( + + + + )} + + ); + } + + renderCard = (props: Dashboard) => { + const menu = ( + + {this.canDelete && ( + + + {t('Are you sure you want to delete')}{' '} + {props.dashboard_title}? + + } + onConfirm={() => this.handleDashboardDelete(props)} + > + {confirmDelete => ( +
+ Delete +
+ )} +
+
+ )} + {this.canExport && ( + this.handleBulkDashboardExport([props])} + > + Export + + )} + {this.canEdit && ( + this.openDashboardEditModal(props)} + > + Edit + + )} +
+ ); + + return ( + {props.published ? 'published' : 'draft'}} + 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 => ( + + ))} + actions={ + + + + + + + } + /> + ); + }; + render() { const { bulkSelectEnabled, @@ -495,6 +593,7 @@ class DashboardList extends React.PureComponent { {dashboardToEdit && ( this.setState({ dashboardToEdit: null })} onSubmit={this.handleDashboardEdit} /> @@ -512,6 +611,7 @@ class DashboardList extends React.PureComponent { 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 a9bebd7f3f7e2..8ee1e5f0a8c35 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 = ({ .map((owner: Owner) => ( @@ -379,7 +375,7 @@ const DatasetList: FunctionComponent = ({ 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 971b54f8cb233..345f8e80724e6 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 6c3792e14d456..09f4e066bd0ca 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", ]