style(listview): various changes to get closer to SIP-34 designs (#11101)

This commit is contained in:
ʈᵃᵢ 2020-10-09 13:06:26 -07:00 committed by GitHub
parent 56d5e8a1cb
commit 7b0dabd7aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 366 additions and 299 deletions

View File

@ -32,7 +32,7 @@ describe('dashboard list view', () => {
// check dashboard list view header // check dashboard list view header
cy.get('th[role="columnheader"]:nth-child(2)').contains('Title'); cy.get('th[role="columnheader"]:nth-child(2)').contains('Title');
cy.get('th[role="columnheader"]:nth-child(3)').contains('Modified By'); cy.get('th[role="columnheader"]:nth-child(3)').contains('Modified By');
cy.get('th[role="columnheader"]:nth-child(4)').contains('Published'); cy.get('th[role="columnheader"]:nth-child(4)').contains('Status');
cy.get('th[role="columnheader"]:nth-child(5)').contains('Modified'); cy.get('th[role="columnheader"]:nth-child(5)').contains('Modified');
cy.get('th[role="columnheader"]:nth-child(6)').contains('Created By'); cy.get('th[role="columnheader"]:nth-child(6)').contains('Created By');
cy.get('th[role="columnheader"]:nth-child(7)').contains('Owners'); cy.get('th[role="columnheader"]:nth-child(7)').contains('Owners');

View File

@ -26,8 +26,6 @@ interface FaveStarProps {
fetchFaveStar(id: number): any; fetchFaveStar(id: number): any;
saveFaveStar(id: number, isStarred: boolean): any; saveFaveStar(id: number, isStarred: boolean): any;
isStarred: boolean; isStarred: boolean;
width?: number;
height?: number;
showTooltip?: boolean; showTooltip?: boolean;
} }
@ -36,10 +34,10 @@ export default class FaveStar extends React.PureComponent<FaveStarProps> {
this.props.fetchFaveStar(this.props.itemId); this.props.fetchFaveStar(this.props.itemId);
} }
onClick(e: React.MouseEvent) { onClick = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.props.saveFaveStar(this.props.itemId, this.props.isStarred); this.props.saveFaveStar(this.props.itemId, this.props.isStarred);
} };
render() { render() {
if (this.props.showTooltip) { if (this.props.showTooltip) {
@ -48,19 +46,13 @@ export default class FaveStar extends React.PureComponent<FaveStarProps> {
label="fave-unfave" label="fave-unfave"
tooltip={t('Click to favorite/unfavorite')} tooltip={t('Click to favorite/unfavorite')}
> >
<a <a href="#" onClick={this.onClick} className="fave-unfave-icon">
href="#"
onClick={this.onClick.bind(this)}
className="fave-unfave-icon"
>
<Icon <Icon
name={ name={
this.props.isStarred this.props.isStarred
? 'favorite-selected' ? 'favorite-selected'
: 'favorite-unselected' : 'favorite-unselected'
} }
width={this.props.width || 20}
height={this.props.height || 'auto'}
/> />
</a> </a>
</TooltipWrapper> </TooltipWrapper>
@ -68,17 +60,11 @@ export default class FaveStar extends React.PureComponent<FaveStarProps> {
} }
return ( return (
<a <a href="#" onClick={this.onClick} className="fave-unfave-icon">
href="#"
onClick={this.onClick.bind(this)}
className="fave-unfave-icon"
>
<Icon <Icon
name={ name={
this.props.isStarred ? 'favorite-selected' : 'favorite-unselected' this.props.isStarred ? 'favorite-selected' : 'favorite-unselected'
} }
width={this.props.width || 20}
height={this.props.height || 'auto'}
/> />
</a> </a>
); );

View File

@ -33,6 +33,10 @@ const CheckboxLabel = styled.label`
margin-bottom: 0; margin-bottom: 0;
`; `;
const IconWithColor = styled(Icon)`
color: ${({ theme }) => theme.colors.primary.dark1};
`;
const HiddenInput = styled.input` const HiddenInput = styled.input`
visibility: none; visibility: none;
`; `;
@ -57,8 +61,8 @@ const IndeterminateCheckbox = React.forwardRef(
return ( return (
<CheckboxLabel title={title}> <CheckboxLabel title={title}>
{indeterminate && <Icon name="checkbox-half" />} {indeterminate && <IconWithColor name="checkbox-half" />}
{!indeterminate && checked && <Icon name="checkbox-on" />} {!indeterminate && checked && <IconWithColor name="checkbox-on" />}
{!indeterminate && !checked && <Icon name="checkbox-off" />} {!indeterminate && !checked && <Icon name="checkbox-off" />}
<HiddenInput <HiddenInput
className="hidden" className="hidden"

View File

@ -33,8 +33,6 @@ const CardContainer = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(459px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(459px, 1fr));
grid-gap: ${({ theme }) => theme.gridUnit * 8}px; grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
padding: ${({ theme }) => theme.gridUnit * 2}px
${({ theme }) => theme.gridUnit * 4}px;
`; `;
const CardWrapper = styled.div` const CardWrapper = styled.div`

View File

@ -30,11 +30,9 @@ const SortTitle = styled.label`
const SortContainer = styled.div` const SortContainer = styled.div`
display: inline-flex; display: inline-flex;
float: right;
font-size: ${({ theme }) => theme.typography.sizes.s}px; font-size: ${({ theme }) => theme.typography.sizes.s}px;
padding: 24px 24px 0 0; padding: ${({ theme }) => theme.gridUnit * 3}px 0 0 0;
position: relative; text-align: left;
top: 8px;
`; `;
interface CardViewSelectSortProps { interface CardViewSelectSortProps {
onChange: (conf: FetchDataConfig) => any; onChange: (conf: FetchDataConfig) => any;

View File

@ -209,9 +209,7 @@ interface UIFiltersProps {
const FilterWrapper = styled.div` const FilterWrapper = styled.div`
display: inline-block; display: inline-block;
padding: ${({ theme }) => theme.gridUnit * 6}px padding: 0 0 ${({ theme }) => theme.gridUnit * 8}px;
${({ theme }) => theme.gridUnit * 4}px
${({ theme }) => theme.gridUnit * 2}px;
`; `;
function UIFilters({ function UIFilters({

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import { t, styled } from '@superset-ui/core'; import { t, styled } from '@superset-ui/core';
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Alert } from 'react-bootstrap'; import { Alert } from 'react-bootstrap';
import { Empty } from 'src/common/components'; import { Empty } from 'src/common/components';
import cx from 'classnames'; import cx from 'classnames';
@ -42,14 +42,21 @@ const ListViewStyles = styled.div`
.superset-list-view { .superset-list-view {
text-align: left; text-align: left;
background-color: white;
border-radius: 4px 0; border-radius: 4px 0;
margin: 0 16px; margin: 0 16px;
padding-bottom: 48px;
.header {
display: flex;
.header-left {
flex: 5;
}
.header-right {
flex: 1;
text-align: right;
}
}
.body { .body {
overflow: scroll;
max-height: 64vh;
} }
} }
@ -57,6 +64,7 @@ const ListViewStyles = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
} }
.row-count-container { .row-count-container {
@ -114,9 +122,8 @@ const bulkSelectColumnConfig = {
}; };
const ViewModeContainer = styled.div` const ViewModeContainer = styled.div`
padding: ${({ theme }) => theme.gridUnit * 6}px 0px padding: 0 ${({ theme }) => theme.gridUnit * 4}px
${({ theme }) => theme.gridUnit * 2}px ${({ theme }) => theme.gridUnit * 8}px 0;
${({ theme }) => theme.gridUnit * 4}px;
display: inline-block; display: inline-block;
position: relative; position: relative;
top: 8px; top: 8px;
@ -250,7 +257,7 @@ function ListView<T extends object = any>({
const filterable = Boolean(filters.length); const filterable = Boolean(filters.length);
if (filterable) { if (filterable) {
const columnAccessors = columns.reduce( const columnAccessors = columns.reduce(
(acc, col) => ({ ...acc, [col.accessor || col.id]: true }), (acc, col) => ({ ...acc, [col.id || col.accessor]: true }),
{}, {},
); );
filters.forEach(f => { filters.forEach(f => {
@ -267,29 +274,38 @@ function ListView<T extends object = any>({
cardViewEnabled ? defaultViewMode : 'table', cardViewEnabled ? defaultViewMode : 'table',
); );
useEffect(() => {
// discard selections if bulk select is disabled
if (!bulkSelectEnabled) toggleAllRowsSelected(false);
}, [bulkSelectEnabled, toggleAllRowsSelected]);
return ( return (
<ListViewStyles> <ListViewStyles>
<div className={`superset-list-view ${className}`}> <div className={`superset-list-view ${className}`}>
<div className="header"> <div className="header">
{cardViewEnabled && ( <div className="header-left">
<ViewModeToggle mode={viewingMode} setMode={setViewingMode} /> {cardViewEnabled && (
)} <ViewModeToggle mode={viewingMode} setMode={setViewingMode} />
{filterable && ( )}
<FilterControls {filterable && (
filters={filters} <FilterControls
internalFilters={internalFilters} filters={filters}
updateFilterValue={applyFilterValue} internalFilters={internalFilters}
/> updateFilterValue={applyFilterValue}
)} />
{viewingMode === 'card' && cardSortSelectOptions && ( )}
<CardSortSelect </div>
initialSort={initialSort} <div className="header-right">
onChange={fetchData} {viewingMode === 'card' && cardSortSelectOptions && (
options={cardSortSelectOptions} <CardSortSelect
pageIndex={pageIndex} initialSort={initialSort}
pageSize={pageSize} onChange={fetchData}
/> options={cardSortSelectOptions}
)} pageIndex={pageIndex}
pageSize={pageSize}
/>
)}
</div>
</div> </div>
<div className="body"> <div className="body">
{bulkSelectEnabled && ( {bulkSelectEnabled && (
@ -318,9 +334,9 @@ function ListView<T extends object = any>({
data-test="bulk-select-action" data-test="bulk-select-action"
key={action.key} key={action.key}
className={cx({ className={cx({
danger: action.type === 'danger', 'btn-danger': action.type === 'danger',
primary: action.type === 'primary', 'btn-primary': action.type === 'primary',
secondary: action.type === 'secondary', 'btn-secondary': action.type === 'secondary',
})} })}
cta cta
onClick={() => onClick={() =>

View File

@ -34,8 +34,19 @@ interface TableCollectionProps {
} }
const Table = styled.table` const Table = styled.table`
background-color: white;
border-collapse: separate; border-collapse: separate;
border-radius: ${({ theme }) => theme.borderRadius}px;
thead > tr > th {
border: 0;
}
tbody {
tr:first-of-type > td {
border-top: 0;
}
}
th { th {
background: ${({ theme }) => theme.colors.grayscale.light5}; background: ${({ theme }) => theme.colors.grayscale.light5};
position: sticky; position: sticky;
@ -177,10 +188,6 @@ const Table = styled.table`
} }
} }
.sort-icon {
position: absolute;
}
@keyframes loading-shimmer { @keyframes loading-shimmer {
40% { 40% {
background-position: 100% 0; background-position: 100% 0;

View File

@ -276,5 +276,6 @@ export const filterSelectStyles: PartialStylesConfig = {
borderWidth: 0, borderWidth: 0,
boxShadow: 'none', boxShadow: 'none',
cursor: 'pointer', cursor: 'pointer',
backgroundColor: 'transparent',
}), }),
}; };

View File

@ -19,6 +19,20 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { styled, logging } from '@superset-ui/core'; import { styled, logging } from '@superset-ui/core';
export type BackgroundPosition = 'top' | 'bottom';
interface ImageContainerProps {
src: string;
position: BackgroundPosition;
}
const ImageContainer = styled.div<ImageContainerProps>`
background-image: url(${({ src }) => src});
background-size: cover;
background-position: center ${({ position }) => position};
display: inline-block;
height: 100%;
width: 100%;
`;
interface ImageLoaderProps interface ImageLoaderProps
extends React.DetailedHTMLProps< extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>, React.HTMLAttributes<HTMLDivElement>,
@ -27,23 +41,14 @@ interface ImageLoaderProps
fallback: string; fallback: string;
src: string; src: string;
isLoading: boolean; isLoading: boolean;
position: BackgroundPosition;
} }
type ImageContainerProps = {
src: string;
};
const ImageContainer = styled.div`
background-image: url(${({ src }: ImageContainerProps) => src});
background-size: cover;
display: inline-block;
height: 100%;
width: 100%;
`;
export default function ImageLoader({ export default function ImageLoader({
src, src,
fallback, fallback,
isLoading, isLoading,
position,
...rest ...rest
}: ImageLoaderProps) { }: ImageLoaderProps) {
const [imgSrc, setImgSrc] = React.useState<string>(fallback); const [imgSrc, setImgSrc] = React.useState<string>(fallback);
@ -71,5 +76,11 @@ export default function ImageLoader({
}; };
}, [src, fallback]); }, [src, fallback]);
return <ImageContainer src={isLoading ? fallback : imgSrc} {...rest} />; return (
<ImageContainer
src={isLoading ? fallback : imgSrc}
{...rest}
position={position}
/>
);
} }

View File

@ -20,7 +20,7 @@ import React from 'react';
import { styled } from '@superset-ui/core'; import { styled } from '@superset-ui/core';
import Icon from 'src/components/Icon'; import Icon from 'src/components/Icon';
import { Card, Skeleton, ThinSkeleton } from 'src/common/components'; import { Card, Skeleton, ThinSkeleton } from 'src/common/components';
import ImageLoader from './ImageLoader'; import ImageLoader, { BackgroundPosition } from './ImageLoader';
const MenuIcon = styled(Icon)` const MenuIcon = styled(Icon)`
width: ${({ theme }) => theme.gridUnit * 4}px; width: ${({ theme }) => theme.gridUnit * 4}px;
@ -36,6 +36,8 @@ const ActionsWrapper = styled.div`
`; `;
const StyledCard = styled(Card)` const StyledCard = styled(Card)`
border: 1px solid #d9dbe4;
.ant-card-body { .ant-card-body {
padding: ${({ theme }) => theme.gridUnit * 4}px padding: ${({ theme }) => theme.gridUnit * 4}px
${({ theme }) => theme.gridUnit * 2}px; ${({ theme }) => theme.gridUnit * 2}px;
@ -43,6 +45,37 @@ const StyledCard = styled(Card)`
.ant-card-meta-detail > div:not(:last-child) { .ant-card-meta-detail > div:not(:last-child) {
margin-bottom: 0; margin-bottom: 0;
} }
.gradient-container {
position: relative;
height: 100%;
}
&:hover {
box-shadow: 8px 8px 28px 0px rgba(0, 0, 0, 0.24);
transition: box-shadow ${({ theme }) => theme.transitionTiming}s ease-in-out;
.gradient-container: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%
);
transition: background ${({ theme }) => theme.transitionTiming}s
ease-in-out;
}
.cover-footer {
transform: translateY(0);
}
}
`; `;
const Cover = styled.div` const Cover = styled.div`
@ -53,33 +86,6 @@ const Cover = styled.div`
transform: translateY(${({ theme }) => theme.gridUnit * 9}px); transform: translateY(${({ theme }) => theme.gridUnit * 9}px);
transition: ${({ theme }) => theme.transitionTiming}s ease-out; transition: ${({ theme }) => theme.transitionTiming}s ease-out;
} }
&:hover {
.cover-footer {
transform: translateY(0);
}
}
`;
const GradientContainer = styled.div`
position: relative;
height: 100%;
&: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 TitleContainer = styled.div` const TitleContainer = styled.div`
@ -139,6 +145,7 @@ interface CardProps {
url?: string; url?: string;
imgURL: string; imgURL: string;
imgFallbackURL: string; imgFallbackURL: string;
imgPosition?: BackgroundPosition;
description: string; description: string;
loading: boolean; loading: boolean;
titleRight?: React.ReactNode; titleRight?: React.ReactNode;
@ -158,19 +165,21 @@ function ListViewCard({
coverRight, coverRight,
actions, actions,
loading, loading,
imgPosition = 'top',
}: CardProps) { }: CardProps) {
return ( return (
<StyledCard <StyledCard
cover={ cover={
<Cover> <Cover>
<a href={url}> <a href={url}>
<GradientContainer> <div className="gradient-container">
<ImageLoader <ImageLoader
src={imgURL} src={imgURL}
fallback={imgFallbackURL} fallback={imgFallbackURL}
isLoading={loading} isLoading={loading}
position={imgPosition}
/> />
</GradientContainer> </div>
</a> </a>
<CoverFooter className="cover-footer"> <CoverFooter className="cover-footer">
{!loading && coverLeft && ( {!loading && coverLeft && (

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React from 'react'; import React, { ReactNode } from 'react';
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from 'react-router-dom';
import { styled } from '@superset-ui/core'; import { styled } from '@superset-ui/core';
import { Nav, Navbar, MenuItem } from 'react-bootstrap'; import { Nav, Navbar, MenuItem } from 'react-bootstrap';
@ -69,7 +69,7 @@ type MenuChild = {
}; };
export interface ButtonProps { export interface ButtonProps {
name: any; name: ReactNode;
onClick: OnClickHandler; onClick: OnClickHandler;
buttonStyle: buttonStyle:
| 'primary' | 'primary'

View File

@ -35,6 +35,7 @@ const SearchInputWrapper = styled.div`
const StyledInput = styled.input` const StyledInput = styled.input`
width: 200px; width: 200px;
height: ${({ theme }) => theme.gridUnit * 8}px;
background-image: none; background-image: none;
border: 1px solid ${({ theme }) => theme.colors.secondary.light2}; border: 1px solid ${({ theme }) => theme.colors.secondary.light2};
border-radius: 4px; border-radius: 4px;
@ -54,14 +55,14 @@ const commonStyles = `
`; `;
const SearchIcon = styled(Icon)` const SearchIcon = styled(Icon)`
${commonStyles}; ${commonStyles};
top: 1px; top: 4px;
left: 2px; left: 2px;
`; `;
const ClearIcon = styled(Icon)` const ClearIcon = styled(Icon)`
${commonStyles}; ${commonStyles};
right: 0px; right: 0px;
top: 1px; top: 4px;
`; `;
export default function SearchInput({ export default function SearchInput({

View File

@ -24,7 +24,7 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils'; import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks'; import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import SubMenu from 'src/components/Menu/SubMenu'; import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import AvatarIcon from 'src/components/AvatarIcon'; import AvatarIcon from 'src/components/AvatarIcon';
import Icon from 'src/components/Icon'; import Icon from 'src/components/Icon';
import FaveStar from 'src/components/FaveStar'; import FaveStar from 'src/components/FaveStar';
@ -39,6 +39,7 @@ import Chart from 'src/types/Chart';
import ListViewCard from 'src/components/ListViewCard'; import ListViewCard from 'src/components/ListViewCard';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import { Dropdown, Menu } from 'src/common/components'; import { Dropdown, Menu } from 'src/common/components';
import TooltipWrapper from 'src/components/TooltipWrapper';
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
const FAVESTAR_BASE_URL = '/superset/favstar/slice'; const FAVESTAR_BASE_URL = '/superset/favstar/slice';
@ -109,6 +110,7 @@ function ChartList(props: ChartListProps) {
setSliceCurrentlyEditing, setSliceCurrentlyEditing,
] = useState<Slice | null>(null); ] = useState<Slice | null>(null);
const canCreate = hasPerm('can_add');
const canEdit = hasPerm('can_edit'); const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete'); const canDelete = hasPerm('can_delete');
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
@ -173,8 +175,6 @@ function ChartList(props: ChartListProps) {
fetchFaveStar={fetchFaveStar} fetchFaveStar={fetchFaveStar}
saveFaveStar={saveFaveStar} saveFaveStar={saveFaveStar}
isStarred={!!favoriteStatusRef.current[id]} isStarred={!!favoriteStatusRef.current[id]}
height={20}
width={20}
/> />
); );
} }
@ -190,6 +190,7 @@ function ChartList(props: ChartListProps) {
Header: '', Header: '',
id: 'favorite', id: 'favorite',
disableSortBy: true, disableSortBy: true,
size: 'xs',
}, },
{ {
Cell: ({ Cell: ({
@ -208,6 +209,7 @@ function ChartList(props: ChartListProps) {
}: any) => vizType, }: any) => vizType,
Header: t('Visualization Type'), Header: t('Visualization Type'),
accessor: 'viz_type', accessor: 'viz_type',
size: 'xxl',
}, },
{ {
Cell: ({ Cell: ({
@ -219,7 +221,8 @@ function ChartList(props: ChartListProps) {
}, },
}: any) => <a href={dsUrl}>{dsNameTxt}</a>, }: any) => <a href={dsUrl}>{dsNameTxt}</a>,
Header: t('Dataset'), Header: t('Dataset'),
accessor: 'datasource_name', accessor: 'datasource_id',
size: 'xl',
}, },
{ {
Cell: ({ Cell: ({
@ -232,6 +235,7 @@ function ChartList(props: ChartListProps) {
}: any) => <a href={changedByUrl}>{changedByName}</a>, }: any) => <a href={changedByUrl}>{changedByName}</a>,
Header: t('Modified By'), Header: t('Modified By'),
accessor: 'changed_by.first_name', accessor: 'changed_by.first_name',
size: 'xl',
}, },
{ {
Cell: ({ Cell: ({
@ -241,22 +245,13 @@ function ChartList(props: ChartListProps) {
}: any) => <span className="no-wrap">{changedOn}</span>, }: any) => <span className="no-wrap">{changedOn}</span>,
Header: t('Last Modified'), Header: t('Last Modified'),
accessor: 'changed_on_delta_humanized', accessor: 'changed_on_delta_humanized',
}, size: 'xl',
{
accessor: 'description',
hidden: true,
disableSortBy: true,
}, },
{ {
accessor: 'owners', accessor: 'owners',
hidden: true, hidden: true,
disableSortBy: true, disableSortBy: true,
}, },
{
accessor: 'datasource_id',
hidden: true,
disableSortBy: true,
},
{ {
Cell: ({ Cell: ({
row: { row: {
@ -267,6 +262,7 @@ function ChartList(props: ChartListProps) {
Header: t('Created By'), Header: t('Created By'),
accessor: 'created_by', accessor: 'created_by',
disableSortBy: true, disableSortBy: true,
size: 'xl',
}, },
{ {
Cell: ({ row: { original } }: any) => { Cell: ({ row: { original } }: any) => {
@ -290,26 +286,38 @@ function ChartList(props: ChartListProps) {
onConfirm={handleDelete} onConfirm={handleDelete}
> >
{confirmDelete => ( {confirmDelete => (
<span <TooltipWrapper
role="button" label="delete-action"
tabIndex={0} tooltip={t('Delete')}
className="action-button" placement="bottom"
onClick={confirmDelete}
> >
<Icon name="trash" /> <span
</span> role="button"
tabIndex={0}
className="action-button"
onClick={confirmDelete}
>
<Icon name="trash" />
</span>
</TooltipWrapper>
)} )}
</ConfirmStatusChange> </ConfirmStatusChange>
)} )}
{canEdit && ( {canEdit && (
<span <TooltipWrapper
role="button" label="edit-action"
tabIndex={0} tooltip={t('Edit')}
className="action-button" placement="bottom"
onClick={openEditModal}
> >
<Icon name="edit-alt" /> <span
</span> role="button"
tabIndex={0}
className="action-button"
onClick={openEditModal}
>
<Icon name="edit-alt" />
</span>
</TooltipWrapper>
)} )}
</span> </span>
); );
@ -319,7 +327,7 @@ function ChartList(props: ChartListProps) {
disableSortBy: true, disableSortBy: true,
}, },
], ],
[canEdit, canDelete, favoriteStatusRef], [canEdit, canDelete],
); );
const filters: Filters = [ const filters: Filters = [
@ -467,6 +475,7 @@ function ChartList(props: ChartListProps) {
url={bulkSelectEnabled ? undefined : chart.url} url={bulkSelectEnabled ? undefined : chart.url}
imgURL={chart.thumbnail_url ?? ''} imgURL={chart.thumbnail_url ?? ''}
imgFallbackURL="/static/assets/images/chart-card-fallback.png" imgFallbackURL="/static/assets/images/chart-card-fallback.png"
imgPosition="bottom"
description={t('Last modified %s', chart.changed_on_delta_humanized)} description={t('Last modified %s', chart.changed_on_delta_humanized)}
coverLeft={(chart.owners || []).slice(0, 5).map(owner => ( coverLeft={(chart.owners || []).slice(0, 5).map(owner => (
<AvatarIcon <AvatarIcon
@ -492,23 +501,31 @@ function ChartList(props: ChartListProps) {
/> />
); );
} }
const subMenuButtons: SubMenuProps['buttons'] = [];
if (canDelete) {
subMenuButtons.push({
name: t('Bulk Select'),
buttonStyle: 'secondary',
onClick: toggleBulkSelect,
});
}
if (canCreate) {
subMenuButtons.push({
name: (
<>
{' '}
<i className="fa fa-plus" /> {t('Chart')}
</>
),
buttonStyle: 'primary',
onClick: () => {
window.location.assign('/chart/add');
},
});
}
return ( return (
<> <>
<SubMenu <SubMenu name={t('Charts')} buttons={subMenuButtons} />
name={t('Charts')}
buttons={
canDelete
? [
{
name: t('Bulk Select'),
buttonStyle: 'secondary',
onClick: toggleBulkSelect,
},
]
: []
}
/>
{sliceCurrentlyEditing && ( {sliceCurrentlyEditing && (
<PropertiesModal <PropertiesModal
onHide={closeChartEditModal} onHide={closeChartEditModal}

View File

@ -23,7 +23,7 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils'; import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks'; import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import SubMenu from 'src/components/Menu/SubMenu'; import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import AvatarIcon from 'src/components/AvatarIcon'; import AvatarIcon from 'src/components/AvatarIcon';
import ListView, { ListViewProps, Filters } from 'src/components/ListView'; import ListView, { ListViewProps, Filters } from 'src/components/ListView';
import ExpandableList from 'src/components/ExpandableList'; import ExpandableList from 'src/components/ExpandableList';
@ -35,6 +35,7 @@ import FaveStar from 'src/components/FaveStar';
import PropertiesModal from 'src/dashboard/components/PropertiesModal'; import PropertiesModal from 'src/dashboard/components/PropertiesModal';
import ListViewCard from 'src/components/ListViewCard'; import ListViewCard from 'src/components/ListViewCard';
import { Dropdown, Menu } from 'src/common/components'; import { Dropdown, Menu } from 'src/common/components';
import TooltipWrapper from 'src/components/TooltipWrapper';
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard'; const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
@ -86,6 +87,7 @@ function DashboardList(props: DashboardListProps) {
null, null,
); );
const canCreate = hasPerm('can_add');
const canEdit = hasPerm('can_edit'); const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete'); const canDelete = hasPerm('can_delete');
const canExport = hasPerm('can_mulexport'); const canExport = hasPerm('can_mulexport');
@ -169,8 +171,6 @@ function DashboardList(props: DashboardListProps) {
fetchFaveStar={fetchFaveStar} fetchFaveStar={fetchFaveStar}
saveFaveStar={saveFaveStar} saveFaveStar={saveFaveStar}
isStarred={!!favoriteStatusRef.current[id]} isStarred={!!favoriteStatusRef.current[id]}
height={20}
width={20}
/> />
); );
} }
@ -186,6 +186,7 @@ function DashboardList(props: DashboardListProps) {
Header: '', Header: '',
id: 'favorite', id: 'favorite',
disableSortBy: true, disableSortBy: true,
size: 'xs',
}, },
{ {
Cell: ({ Cell: ({
@ -208,19 +209,17 @@ function DashboardList(props: DashboardListProps) {
}: any) => <a href={changedByUrl}>{changedByName}</a>, }: any) => <a href={changedByUrl}>{changedByName}</a>,
Header: t('Modified By'), Header: t('Modified By'),
accessor: 'changed_by.first_name', accessor: 'changed_by.first_name',
size: 'xl',
}, },
{ {
Cell: ({ Cell: ({
row: { row: {
original: { published }, original: { published },
}, },
}: any) => ( }: any) => (published ? t('Published') : t('Draft')),
<span className="no-wrap"> Header: t('Status'),
{published ? <Icon name="check" /> : ''}
</span>
),
Header: t('Published'),
accessor: 'published', accessor: 'published',
size: 'xl',
}, },
{ {
Cell: ({ Cell: ({
@ -230,11 +229,7 @@ function DashboardList(props: DashboardListProps) {
}: any) => <span className="no-wrap">{changedOn}</span>, }: any) => <span className="no-wrap">{changedOn}</span>,
Header: t('Modified'), Header: t('Modified'),
accessor: 'changed_on_delta_humanized', accessor: 'changed_on_delta_humanized',
}, size: 'xl',
{
accessor: 'slug',
hidden: true,
disableSortBy: true,
}, },
{ {
Cell: ({ Cell: ({
@ -246,6 +241,7 @@ function DashboardList(props: DashboardListProps) {
Header: t('Created By'), Header: t('Created By'),
accessor: 'created_by', accessor: 'created_by',
disableSortBy: true, disableSortBy: true,
size: 'xl',
}, },
{ {
Cell: ({ Cell: ({
@ -264,6 +260,7 @@ function DashboardList(props: DashboardListProps) {
Header: t('Owners'), Header: t('Owners'),
accessor: 'owners', accessor: 'owners',
disableSortBy: true, disableSortBy: true,
size: 'xl',
}, },
{ {
Cell: ({ row: { original } }: any) => { Cell: ({ row: { original } }: any) => {
@ -287,36 +284,54 @@ function DashboardList(props: DashboardListProps) {
onConfirm={handleDelete} onConfirm={handleDelete}
> >
{confirmDelete => ( {confirmDelete => (
<span <TooltipWrapper
role="button" label="delete-action"
tabIndex={0} tooltip={t('Delete')}
className="action-button" placement="bottom"
onClick={confirmDelete}
> >
<Icon name="trash" /> <span
</span> role="button"
tabIndex={0}
className="action-button"
onClick={confirmDelete}
>
<Icon name="trash" />
</span>
</TooltipWrapper>
)} )}
</ConfirmStatusChange> </ConfirmStatusChange>
)} )}
{canExport && ( {canExport && (
<span <TooltipWrapper
role="button" label="export-action"
tabIndex={0} tooltip={t('Export')}
className="action-button" placement="bottom"
onClick={handleExport}
> >
<Icon name="share" /> <span
</span> role="button"
tabIndex={0}
className="action-button"
onClick={handleExport}
>
<Icon name="share" />
</span>
</TooltipWrapper>
)} )}
{canEdit && ( {canEdit && (
<span <TooltipWrapper
role="button" label="edit-action"
tabIndex={0} tooltip={t('Edit')}
className="action-button" placement="bottom"
onClick={handleEdit}
> >
<Icon name="edit-alt" /> <span
</span> role="button"
tabIndex={0}
className="action-button"
onClick={handleEdit}
>
<Icon name="edit-alt" />
</span>
</TooltipWrapper>
)} )}
</span> </span>
); );
@ -331,7 +346,7 @@ function DashboardList(props: DashboardListProps) {
const filters: Filters = [ const filters: Filters = [
{ {
Header: 'Owner', Header: t('Owner'),
id: 'owners', id: 'owners',
input: 'select', input: 'select',
operator: 'rel_m_m', operator: 'rel_m_m',
@ -371,18 +386,18 @@ function DashboardList(props: DashboardListProps) {
paginate: true, paginate: true,
}, },
{ {
Header: 'Published', Header: t('Status'),
id: 'published', id: 'published',
input: 'select', input: 'select',
operator: 'eq', operator: 'eq',
unfilteredLabel: 'Any', unfilteredLabel: 'Any',
selects: [ selects: [
{ label: 'Published', value: true }, { label: t('Published'), value: true },
{ label: 'Unpublished', value: false }, { label: t('Unpublished'), value: false },
], ],
}, },
{ {
Header: 'Search', Header: t('Search'),
id: 'dashboard_title', id: 'dashboard_title',
input: 'search', input: 'search',
operator: 'title_or_slug', operator: 'title_or_slug',
@ -495,22 +510,31 @@ function DashboardList(props: DashboardListProps) {
); );
} }
const subMenuButtons: SubMenuProps['buttons'] = [];
if (canDelete || canExport) {
subMenuButtons.push({
name: t('Bulk Select'),
buttonStyle: 'secondary',
onClick: toggleBulkSelect,
});
}
if (canCreate) {
subMenuButtons.push({
name: (
<>
{' '}
<i className="fa fa-plus" /> {t('Dashboard')}
</>
),
buttonStyle: 'primary',
onClick: () => {
window.location.assign('/dashboard/new');
},
});
}
return ( return (
<> <>
<SubMenu <SubMenu name={t('Dashboards')} buttons={subMenuButtons} />
name={t('Dashboards')}
buttons={
canDelete || canExport
? [
{
name: t('Bulk Select'),
buttonStyle: 'secondary',
onClick: toggleBulkSelect,
},
]
: undefined
}
/>
<ConfirmStatusChange <ConfirmStatusChange
title={t('Please confirm')} title={t('Please confirm')}
description={t( description={t(

View File

@ -285,7 +285,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
disableSortBy: true, disableSortBy: true,
}, },
], ],
[canDelete, canCreate], [canDelete, canEdit],
); );
const filters: Filters = useMemo( const filters: Filters = useMemo(

View File

@ -17,7 +17,12 @@
* under the License. * under the License.
*/ */
import { SupersetClient, t } from '@superset-ui/core'; import { SupersetClient, t } from '@superset-ui/core';
import React, { FunctionComponent, useState, useMemo } from 'react'; import React, {
FunctionComponent,
useState,
useMemo,
useCallback,
} from 'react';
import rison from 'rison'; import rison from 'rison';
import { import {
createFetchRelated, createFetchRelated,
@ -101,20 +106,23 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
const openDatasetEditModal = ({ id }: Dataset) => { const openDatasetEditModal = useCallback(
SupersetClient.get({ ({ id }: Dataset) => {
endpoint: `/api/v1/dataset/${id}`, SupersetClient.get({
}) endpoint: `/api/v1/dataset/${id}`,
.then(({ json = {} }) => {
const owners = json.result.owners.map((owner: any) => owner.id);
setDatasetCurrentlyEditing({ ...json.result, owners });
}) })
.catch(() => { .then(({ json = {} }) => {
addDangerToast( const owners = json.result.owners.map((owner: any) => owner.id);
t('An error occurred while fetching dataset related data'), setDatasetCurrentlyEditing({ ...json.result, owners });
); })
}); .catch(() => {
}; addDangerToast(
t('An error occurred while fetching dataset related data'),
);
});
},
[addDangerToast],
);
const openDatasetDeleteModal = (dataset: Dataset) => const openDatasetDeleteModal = (dataset: Dataset) =>
SupersetClient.get({ SupersetClient.get({
@ -170,9 +178,9 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
{ {
Cell: ({ Cell: ({
row: { row: {
original: { table_name: datasetTitle }, original: { table_name: datasetTitle, explore_url: exploreURL },
}, },
}: any) => datasetTitle, }: any) => <a href={exploreURL}>{datasetTitle}</a>,
Header: t('Name'), Header: t('Name'),
accessor: 'table_name', accessor: 'table_name',
}, },
@ -263,20 +271,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
} }
return ( return (
<span className="actions"> <span className="actions">
<TooltipWrapper
label="explore-action"
tooltip={t('Explore')}
placement="bottom"
>
<a
role="button"
tabIndex={0}
className="action-button"
href={original.explore_url}
>
<Icon name="nav-explore" />
</a>
</TooltipWrapper>
{canDelete && ( {canDelete && (
<TooltipWrapper <TooltipWrapper
label="delete-action" label="delete-action"
@ -318,7 +312,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
disableSortBy: true, disableSortBy: true,
}, },
], ],
[canCreate, canEdit, canDelete], [canEdit, canDelete, openDatasetEditModal],
); );
const filterTypes: Filters = useMemo( const filterTypes: Filters = useMemo(

View File

@ -18,7 +18,7 @@
*/ */
import { SupersetClient, t, styled } from '@superset-ui/core'; import { SupersetClient, t, styled } from '@superset-ui/core';
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo, useCallback } from 'react';
import rison from 'rison'; import rison from 'rison';
import moment from 'moment'; import moment from 'moment';
import { import {
@ -104,7 +104,6 @@ function SavedQueryList({
setSavedQueryCurrentlyPreviewing, setSavedQueryCurrentlyPreviewing,
] = useState<SavedQueryObject | null>(null); ] = useState<SavedQueryObject | null>(null);
const canCreate = hasPerm('can_add');
const canEdit = hasPerm('can_edit'); const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete'); const canDelete = hasPerm('can_delete');
@ -112,20 +111,23 @@ function SavedQueryList({
window.open(`${window.location.origin}/superset/sqllab?new=true`); window.open(`${window.location.origin}/superset/sqllab?new=true`);
}; };
const handleSavedQueryPreview = (id: number) => { const handleSavedQueryPreview = useCallback(
SupersetClient.get({ (id: number) => {
endpoint: `/api/v1/saved_query/${id}`, SupersetClient.get({
}).then( endpoint: `/api/v1/saved_query/${id}`,
({ json = {} }) => { }).then(
setSavedQueryCurrentlyPreviewing({ ...json.result }); ({ json = {} }) => {
}, setSavedQueryCurrentlyPreviewing({ ...json.result });
createErrorHandler(errMsg => },
addDangerToast( createErrorHandler(errMsg =>
t('There was an issue previewing the selected query %s', errMsg), addDangerToast(
t('There was an issue previewing the selected query %s', errMsg),
),
), ),
), );
); },
}; [addDangerToast],
);
const menuData: SubMenuProps = { const menuData: SubMenuProps = {
activeChild: 'Saved Queries', activeChild: 'Saved Queries',
@ -155,41 +157,44 @@ function SavedQueryList({
window.open(`${window.location.origin}/superset/sqllab?savedQueryId=${id}`); window.open(`${window.location.origin}/superset/sqllab?savedQueryId=${id}`);
}; };
const copyQueryLink = (id: number) => { const copyQueryLink = useCallback(
const selection: Selection | null = document.getSelection(); (id: number) => {
const selection: Selection | null = document.getSelection();
if (selection) { if (selection) {
selection.removeAllRanges();
const range = document.createRange();
const span = document.createElement('span');
span.textContent = `${window.location.origin}/superset/sqllab?savedQueryId=${id}`;
span.style.position = 'fixed';
span.style.top = '0';
span.style.clip = 'rect(0, 0, 0, 0)';
span.style.whiteSpace = 'pre';
document.body.appendChild(span);
range.selectNode(span);
selection.addRange(range);
try {
if (!document.execCommand('copy')) {
throw new Error(t('Not successful'));
}
} catch (err) {
addDangerToast(t('Sorry, your browser does not support copying.'));
}
document.body.removeChild(span);
if (selection.removeRange) {
selection.removeRange(range);
} else {
selection.removeAllRanges(); selection.removeAllRanges();
} const range = document.createRange();
const span = document.createElement('span');
span.textContent = `${window.location.origin}/superset/sqllab?savedQueryId=${id}`;
span.style.position = 'fixed';
span.style.top = '0';
span.style.clip = 'rect(0, 0, 0, 0)';
span.style.whiteSpace = 'pre';
addSuccessToast(t('Link Copied!')); document.body.appendChild(span);
} range.selectNode(span);
}; selection.addRange(range);
try {
if (!document.execCommand('copy')) {
throw new Error(t('Not successful'));
}
} catch (err) {
addDangerToast(t('Sorry, your browser does not support copying.'));
}
document.body.removeChild(span);
if (selection.removeRange) {
selection.removeRange(range);
} else {
selection.removeAllRanges();
}
addSuccessToast(t('Link Copied!'));
}
},
[addDangerToast, addSuccessToast],
);
const handleQueryDelete = ({ id, label }: SavedQueryObject) => { const handleQueryDelete = ({ id, label }: SavedQueryObject) => {
SupersetClient.delete({ SupersetClient.delete({
@ -232,22 +237,15 @@ function SavedQueryList({
Header: t('Name'), Header: t('Name'),
}, },
{ {
id: 'database',
accessor: 'database.database_name', accessor: 'database.database_name',
Header: t('Database'), Header: t('Database'),
}, size: 'xl',
{
accessor: 'database',
hidden: true,
disableSortBy: true,
Cell: ({
row: {
original: { database },
},
}: any) => `${database.database_name}`,
}, },
{ {
accessor: 'schema', accessor: 'schema',
Header: t('Schema'), Header: t('Schema'),
size: 'xl',
}, },
{ {
Cell: ({ Cell: ({
@ -284,6 +282,7 @@ function SavedQueryList({
}, },
accessor: 'sql_tables', accessor: 'sql_tables',
Header: t('Tables'), Header: t('Tables'),
size: 'xl',
disableSortBy: true, disableSortBy: true,
}, },
{ {
@ -309,6 +308,7 @@ function SavedQueryList({
}, },
Header: t('Created On'), Header: t('Created On'),
accessor: 'created_on', accessor: 'created_on',
size: 'xl',
}, },
{ {
Cell: ({ Cell: ({
@ -318,6 +318,7 @@ function SavedQueryList({
}: any) => changedOn, }: any) => changedOn,
Header: t('Modified'), Header: t('Modified'),
accessor: 'changed_on_delta_humanized', accessor: 'changed_on_delta_humanized',
size: 'xl',
}, },
{ {
Cell: ({ row: { original } }: any) => { Cell: ({ row: { original } }: any) => {
@ -374,7 +375,7 @@ function SavedQueryList({
disableSortBy: true, disableSortBy: true,
}, },
], ],
[canDelete, canCreate], [canDelete, canEdit, copyQueryLink, handleSavedQueryPreview],
); );
const filters: Filters = useMemo( const filters: Filters = useMemo(

View File

@ -76,6 +76,8 @@ input.form-control {
.container-fluid { .container-fluid {
text-align: left; text-align: left;
padding-left: 16px;
padding-right: 16px;
} }
input[type='checkbox'] { input[type='checkbox'] {