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
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(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(6)').contains('Created By');
cy.get('th[role="columnheader"]:nth-child(7)').contains('Owners');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,20 @@
import React, { useEffect } from 'react';
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
extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
@ -27,23 +41,14 @@ interface ImageLoaderProps
fallback: string;
src: string;
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({
src,
fallback,
isLoading,
position,
...rest
}: ImageLoaderProps) {
const [imgSrc, setImgSrc] = React.useState<string>(fallback);
@ -71,5 +76,11 @@ export default function ImageLoader({
};
}, [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 Icon from 'src/components/Icon';
import { Card, Skeleton, ThinSkeleton } from 'src/common/components';
import ImageLoader from './ImageLoader';
import ImageLoader, { BackgroundPosition } from './ImageLoader';
const MenuIcon = styled(Icon)`
width: ${({ theme }) => theme.gridUnit * 4}px;
@ -36,6 +36,8 @@ const ActionsWrapper = styled.div`
`;
const StyledCard = styled(Card)`
border: 1px solid #d9dbe4;
.ant-card-body {
padding: ${({ theme }) => theme.gridUnit * 4}px
${({ theme }) => theme.gridUnit * 2}px;
@ -43,6 +45,37 @@ const StyledCard = styled(Card)`
.ant-card-meta-detail > div:not(:last-child) {
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`
@ -53,33 +86,6 @@ const Cover = styled.div`
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;
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`
@ -139,6 +145,7 @@ interface CardProps {
url?: string;
imgURL: string;
imgFallbackURL: string;
imgPosition?: BackgroundPosition;
description: string;
loading: boolean;
titleRight?: React.ReactNode;
@ -158,19 +165,21 @@ function ListViewCard({
coverRight,
actions,
loading,
imgPosition = 'top',
}: CardProps) {
return (
<StyledCard
cover={
<Cover>
<a href={url}>
<GradientContainer>
<div className="gradient-container">
<ImageLoader
src={imgURL}
fallback={imgFallbackURL}
isLoading={loading}
position={imgPosition}
/>
</GradientContainer>
</div>
</a>
<CoverFooter className="cover-footer">
{!loading && coverLeft && (

View File

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

View File

@ -35,6 +35,7 @@ const SearchInputWrapper = styled.div`
const StyledInput = styled.input`
width: 200px;
height: ${({ theme }) => theme.gridUnit * 8}px;
background-image: none;
border: 1px solid ${({ theme }) => theme.colors.secondary.light2};
border-radius: 4px;
@ -54,14 +55,14 @@ const commonStyles = `
`;
const SearchIcon = styled(Icon)`
${commonStyles};
top: 1px;
top: 4px;
left: 2px;
`;
const ClearIcon = styled(Icon)`
${commonStyles};
right: 0px;
top: 1px;
top: 4px;
`;
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 { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
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 Icon from 'src/components/Icon';
import FaveStar from 'src/components/FaveStar';
@ -39,6 +39,7 @@ import Chart from 'src/types/Chart';
import ListViewCard from 'src/components/ListViewCard';
import Label from 'src/components/Label';
import { Dropdown, Menu } from 'src/common/components';
import TooltipWrapper from 'src/components/TooltipWrapper';
const PAGE_SIZE = 25;
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
@ -109,6 +110,7 @@ function ChartList(props: ChartListProps) {
setSliceCurrentlyEditing,
] = useState<Slice | null>(null);
const canCreate = hasPerm('can_add');
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
@ -173,8 +175,6 @@ function ChartList(props: ChartListProps) {
fetchFaveStar={fetchFaveStar}
saveFaveStar={saveFaveStar}
isStarred={!!favoriteStatusRef.current[id]}
height={20}
width={20}
/>
);
}
@ -190,6 +190,7 @@ function ChartList(props: ChartListProps) {
Header: '',
id: 'favorite',
disableSortBy: true,
size: 'xs',
},
{
Cell: ({
@ -208,6 +209,7 @@ function ChartList(props: ChartListProps) {
}: any) => vizType,
Header: t('Visualization Type'),
accessor: 'viz_type',
size: 'xxl',
},
{
Cell: ({
@ -219,7 +221,8 @@ function ChartList(props: ChartListProps) {
},
}: any) => <a href={dsUrl}>{dsNameTxt}</a>,
Header: t('Dataset'),
accessor: 'datasource_name',
accessor: 'datasource_id',
size: 'xl',
},
{
Cell: ({
@ -232,6 +235,7 @@ function ChartList(props: ChartListProps) {
}: any) => <a href={changedByUrl}>{changedByName}</a>,
Header: t('Modified By'),
accessor: 'changed_by.first_name',
size: 'xl',
},
{
Cell: ({
@ -241,22 +245,13 @@ function ChartList(props: ChartListProps) {
}: any) => <span className="no-wrap">{changedOn}</span>,
Header: t('Last Modified'),
accessor: 'changed_on_delta_humanized',
},
{
accessor: 'description',
hidden: true,
disableSortBy: true,
size: 'xl',
},
{
accessor: 'owners',
hidden: true,
disableSortBy: true,
},
{
accessor: 'datasource_id',
hidden: true,
disableSortBy: true,
},
{
Cell: ({
row: {
@ -267,6 +262,7 @@ function ChartList(props: ChartListProps) {
Header: t('Created By'),
accessor: 'created_by',
disableSortBy: true,
size: 'xl',
},
{
Cell: ({ row: { original } }: any) => {
@ -290,26 +286,38 @@ function ChartList(props: ChartListProps) {
onConfirm={handleDelete}
>
{confirmDelete => (
<span
role="button"
tabIndex={0}
className="action-button"
onClick={confirmDelete}
<TooltipWrapper
label="delete-action"
tooltip={t('Delete')}
placement="bottom"
>
<Icon name="trash" />
</span>
<span
role="button"
tabIndex={0}
className="action-button"
onClick={confirmDelete}
>
<Icon name="trash" />
</span>
</TooltipWrapper>
)}
</ConfirmStatusChange>
)}
{canEdit && (
<span
role="button"
tabIndex={0}
className="action-button"
onClick={openEditModal}
<TooltipWrapper
label="edit-action"
tooltip={t('Edit')}
placement="bottom"
>
<Icon name="edit-alt" />
</span>
<span
role="button"
tabIndex={0}
className="action-button"
onClick={openEditModal}
>
<Icon name="edit-alt" />
</span>
</TooltipWrapper>
)}
</span>
);
@ -319,7 +327,7 @@ function ChartList(props: ChartListProps) {
disableSortBy: true,
},
],
[canEdit, canDelete, favoriteStatusRef],
[canEdit, canDelete],
);
const filters: Filters = [
@ -467,6 +475,7 @@ function ChartList(props: ChartListProps) {
url={bulkSelectEnabled ? undefined : chart.url}
imgURL={chart.thumbnail_url ?? ''}
imgFallbackURL="/static/assets/images/chart-card-fallback.png"
imgPosition="bottom"
description={t('Last modified %s', chart.changed_on_delta_humanized)}
coverLeft={(chart.owners || []).slice(0, 5).map(owner => (
<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 (
<>
<SubMenu
name={t('Charts')}
buttons={
canDelete
? [
{
name: t('Bulk Select'),
buttonStyle: 'secondary',
onClick: toggleBulkSelect,
},
]
: []
}
/>
<SubMenu name={t('Charts')} buttons={subMenuButtons} />
{sliceCurrentlyEditing && (
<PropertiesModal
onHide={closeChartEditModal}

View File

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

View File

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

View File

@ -17,7 +17,12 @@
* under the License.
*/
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 {
createFetchRelated,
@ -101,20 +106,23 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
const openDatasetEditModal = ({ id }: Dataset) => {
SupersetClient.get({
endpoint: `/api/v1/dataset/${id}`,
})
.then(({ json = {} }) => {
const owners = json.result.owners.map((owner: any) => owner.id);
setDatasetCurrentlyEditing({ ...json.result, owners });
const openDatasetEditModal = useCallback(
({ id }: Dataset) => {
SupersetClient.get({
endpoint: `/api/v1/dataset/${id}`,
})
.catch(() => {
addDangerToast(
t('An error occurred while fetching dataset related data'),
);
});
};
.then(({ json = {} }) => {
const owners = json.result.owners.map((owner: any) => owner.id);
setDatasetCurrentlyEditing({ ...json.result, owners });
})
.catch(() => {
addDangerToast(
t('An error occurred while fetching dataset related data'),
);
});
},
[addDangerToast],
);
const openDatasetDeleteModal = (dataset: Dataset) =>
SupersetClient.get({
@ -170,9 +178,9 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
{
Cell: ({
row: {
original: { table_name: datasetTitle },
original: { table_name: datasetTitle, explore_url: exploreURL },
},
}: any) => datasetTitle,
}: any) => <a href={exploreURL}>{datasetTitle}</a>,
Header: t('Name'),
accessor: 'table_name',
},
@ -263,20 +271,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
}
return (
<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 && (
<TooltipWrapper
label="delete-action"
@ -318,7 +312,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
disableSortBy: true,
},
],
[canCreate, canEdit, canDelete],
[canEdit, canDelete, openDatasetEditModal],
);
const filterTypes: Filters = useMemo(

View File

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

View File

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