mirror of https://github.com/apache/superset.git
feat(dashboard): Add thumbnails to dashboard edit draggable chart list (#20528)
* Add chart thumbnails to dashboard edit draggable charts. * Reorganize hierarchy and add tests. * Incorporate review suggestions. * Update design and add tooltips. * Fix missing thumbnails. * Fix tests. * Fix moving viz type label. * Convert AddSliceCard to TS, update hierarchy.
This commit is contained in:
parent
fe91974163
commit
d50784dd80
|
@ -72,6 +72,7 @@ export function fetchSlices(
|
|||
'id',
|
||||
'params',
|
||||
'slice_name',
|
||||
'thumbnail_url',
|
||||
'url',
|
||||
'viz_type',
|
||||
],
|
||||
|
@ -114,6 +115,7 @@ export function fetchSlices(
|
|||
viz_type: slice.viz_type,
|
||||
modified: slice.changed_on_delta_humanized,
|
||||
changed_on_humanized: slice.changed_on_delta_humanized,
|
||||
thumbnail_url: slice.thumbnail_url,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,148 +0,0 @@
|
|||
/**
|
||||
* 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 cx from 'classnames';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
|
||||
const propTypes = {
|
||||
datasourceUrl: PropTypes.string,
|
||||
datasourceName: PropTypes.string,
|
||||
innerRef: PropTypes.func,
|
||||
isSelected: PropTypes.bool,
|
||||
lastModified: PropTypes.string,
|
||||
sliceName: PropTypes.string.isRequired,
|
||||
style: PropTypes.object,
|
||||
visType: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
datasourceUrl: null,
|
||||
datasourceName: '-',
|
||||
innerRef: null,
|
||||
isSelected: false,
|
||||
style: null,
|
||||
lastModified: null,
|
||||
};
|
||||
|
||||
const Styled = styled.div`
|
||||
${({ theme }) => `
|
||||
.chart-card {
|
||||
border: 1px solid ${theme.colors.grayscale.light2};
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
background: ${theme.colors.grayscale.light5};
|
||||
padding: ${theme.gridUnit * 2}px;
|
||||
margin: 0 ${theme.gridUnit * 3}px
|
||||
${theme.gridUnit * 3}px
|
||||
${theme.gridUnit * 3}px;
|
||||
position: relative;
|
||||
cursor: move;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.grayscale.light4};
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card.is-selected {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin-right: 60px;
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
font-weight: ${theme.typography.weights.bold};
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.item {
|
||||
span {
|
||||
word-break: break-all;
|
||||
|
||||
&:first-child {
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-added-label {
|
||||
background: ${theme.colors.grayscale.base};
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
color: ${theme.colors.grayscale.light5};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
padding: ${theme.gridUnit}px
|
||||
${theme.gridUnit * 2}px;
|
||||
top: ${theme.gridUnit * 8}px;
|
||||
right: ${theme.gridUnit * 8}px;
|
||||
pointer-events: none;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
function AddSliceCard({
|
||||
datasourceUrl,
|
||||
datasourceName,
|
||||
innerRef,
|
||||
isSelected,
|
||||
lastModified,
|
||||
sliceName,
|
||||
style,
|
||||
visType,
|
||||
}) {
|
||||
return (
|
||||
<Styled ref={innerRef} style={style}>
|
||||
<div
|
||||
className={cx('chart-card', isSelected && 'is-selected')}
|
||||
data-test="chart-card"
|
||||
>
|
||||
<div className="card-title" data-test="card-title">
|
||||
{sliceName}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="item">
|
||||
<span>{t('Modified')} </span>
|
||||
<span>{lastModified}</span>
|
||||
</div>
|
||||
<div className="item">
|
||||
<span>{t('Visualization')} </span>
|
||||
<span>{visType}</span>
|
||||
</div>
|
||||
<div className="item">
|
||||
<span>{t('Data source')} </span>
|
||||
<a href={datasourceUrl}>{datasourceName}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && <div className="is-added-label">{t('Added')}</div>}
|
||||
</Styled>
|
||||
);
|
||||
}
|
||||
|
||||
AddSliceCard.propTypes = propTypes;
|
||||
AddSliceCard.defaultProps = defaultProps;
|
||||
|
||||
export default AddSliceCard;
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* 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 { FeatureFlag } from '@superset-ui/core';
|
||||
import { act, render, screen } from 'spec/helpers/testing-library';
|
||||
import AddSliceCard from '.';
|
||||
|
||||
jest.mock('src/components/DynamicPlugins', () => ({
|
||||
usePluginContext: () => ({
|
||||
mountedPluginMetadata: { table: { name: 'Table' } },
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockedProps = {
|
||||
visType: 'table',
|
||||
sliceName: '-',
|
||||
};
|
||||
|
||||
declare const global: {
|
||||
featureFlags: Record<string, boolean>;
|
||||
};
|
||||
|
||||
test('do not render thumbnail if feature flag is not set', async () => {
|
||||
global.featureFlags = {
|
||||
[FeatureFlag.THUMBNAILS]: false,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(<AddSliceCard {...mockedProps} />);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('thumbnail')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('render thumbnail if feature flag is set', async () => {
|
||||
global.featureFlags = {
|
||||
[FeatureFlag.THUMBNAILS]: true,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
render(<AddSliceCard {...mockedProps} />);
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('thumbnail')).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,279 @@
|
|||
/**
|
||||
* 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, {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { t, isFeatureEnabled, FeatureFlag, css } from '@superset-ui/core';
|
||||
import ImageLoader from 'src/components/ListViewCard/ImageLoader';
|
||||
import { usePluginContext } from 'src/components/DynamicPlugins';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { Theme } from '@emotion/react';
|
||||
|
||||
const FALLBACK_THUMBNAIL_URL = '/static/assets/images/chart-card-fallback.svg';
|
||||
|
||||
const TruncatedTextWithTooltip: React.FC = ({ children, ...props }) => {
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
setIsTruncated(
|
||||
ref.current ? ref.current.offsetWidth < ref.current.scrollWidth : false,
|
||||
);
|
||||
}, [children]);
|
||||
|
||||
const div = (
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
css={css`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return isTruncated ? <Tooltip title={children}>{div}</Tooltip> : div;
|
||||
};
|
||||
|
||||
const MetadataItem: React.FC<{
|
||||
label: ReactNode;
|
||||
value: ReactNode;
|
||||
}> = ({ label, value }) => (
|
||||
<div
|
||||
css={(theme: Theme) => css`
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: ${theme.gridUnit}px;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={(theme: Theme) => css`
|
||||
margin-right: ${theme.gridUnit * 4}px;
|
||||
color: ${theme.colors.grayscale.base};
|
||||
`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
css={css`
|
||||
min-width: 0;
|
||||
`}
|
||||
>
|
||||
<TruncatedTextWithTooltip>{value}</TruncatedTextWithTooltip>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const SliceAddedBadgePlaceholder: React.FC<{
|
||||
showThumbnails?: boolean;
|
||||
placeholderRef: (element: HTMLDivElement) => void;
|
||||
}> = ({ showThumbnails, placeholderRef }) => (
|
||||
<div
|
||||
ref={placeholderRef}
|
||||
css={(theme: Theme) => css`
|
||||
/* Display styles */
|
||||
border: 1px solid ${theme.colors.primary.dark1};
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
color: ${theme.colors.primary.dark1};
|
||||
font-size: ${theme.typography.sizes.xs}px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
padding: ${theme.gridUnit / 2}px ${theme.gridUnit * 2}px;
|
||||
margin-left: ${theme.gridUnit * 4}px;
|
||||
pointer-events: none;
|
||||
|
||||
/* Position styles */
|
||||
visibility: hidden;
|
||||
position: ${showThumbnails ? 'absolute' : 'unset'};
|
||||
top: ${showThumbnails ? '72px' : 'unset'};
|
||||
left: ${showThumbnails ? '84px' : 'unset'};
|
||||
`}
|
||||
>
|
||||
{t('Added')}
|
||||
</div>
|
||||
);
|
||||
|
||||
const SliceAddedBadge: React.FC<{ placeholder?: HTMLDivElement }> = ({
|
||||
placeholder,
|
||||
}) => (
|
||||
<div
|
||||
css={(theme: Theme) => css`
|
||||
/* Display styles */
|
||||
border: 1px solid ${theme.colors.primary.dark1};
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
color: ${theme.colors.primary.dark1};
|
||||
font-size: ${theme.typography.sizes.xs}px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
padding: ${theme.gridUnit / 2}px ${theme.gridUnit * 2}px;
|
||||
margin-left: ${theme.gridUnit * 4}px;
|
||||
pointer-events: none;
|
||||
|
||||
/* Position styles */
|
||||
display: ${placeholder ? 'unset' : 'none'};
|
||||
position: absolute;
|
||||
top: ${placeholder ? `${placeholder.offsetTop}px` : 'unset'};
|
||||
left: ${placeholder ? `${placeholder.offsetLeft - 2}px` : 'unset'};
|
||||
`}
|
||||
>
|
||||
{t('Added')}
|
||||
</div>
|
||||
);
|
||||
|
||||
const AddSliceCard: React.FC<{
|
||||
datasourceUrl?: string;
|
||||
datasourceName?: string;
|
||||
innerRef?: React.RefObject<HTMLDivElement>;
|
||||
isSelected?: boolean;
|
||||
lastModified?: string;
|
||||
sliceName: string;
|
||||
style?: CSSProperties;
|
||||
thumbnailUrl?: string;
|
||||
visType: string;
|
||||
}> = ({
|
||||
datasourceUrl,
|
||||
datasourceName = '-',
|
||||
innerRef,
|
||||
isSelected = false,
|
||||
lastModified,
|
||||
sliceName,
|
||||
style = {},
|
||||
thumbnailUrl,
|
||||
visType,
|
||||
}) => {
|
||||
const showThumbnails = isFeatureEnabled(FeatureFlag.THUMBNAILS);
|
||||
const [sliceAddedBadge, setSliceAddedBadge] = useState<HTMLDivElement>();
|
||||
const { mountedPluginMetadata } = usePluginContext();
|
||||
const vizName = useMemo(
|
||||
() => mountedPluginMetadata[visType].name,
|
||||
[mountedPluginMetadata, visType],
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={innerRef} style={style}>
|
||||
<div
|
||||
data-test="chart-card"
|
||||
css={(theme: Theme) => css`
|
||||
border: 1px solid ${theme.colors.grayscale.light2};
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
background: ${theme.colors.grayscale.light5};
|
||||
padding: ${theme.gridUnit * 4}px;
|
||||
margin: 0 ${theme.gridUnit * 3}px
|
||||
${theme.gridUnit * 3}px
|
||||
${theme.gridUnit * 3}px;
|
||||
position: relative;
|
||||
cursor: ${isSelected ? 'not-allowed' : 'move'};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
line-height: 1.3;
|
||||
color: ${theme.colors.grayscale.dark1}
|
||||
|
||||
&:hover {
|
||||
background: ${theme.colors.grayscale.light4};
|
||||
}
|
||||
|
||||
opacity: ${isSelected ? 0.4 : 'unset'};
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
`}
|
||||
>
|
||||
{showThumbnails ? (
|
||||
<div
|
||||
data-test="thumbnail"
|
||||
css={css`
|
||||
width: 146px;
|
||||
height: 82px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 16px;
|
||||
`}
|
||||
>
|
||||
<ImageLoader
|
||||
src={thumbnailUrl || ''}
|
||||
fallback={FALLBACK_THUMBNAIL_URL}
|
||||
position="top"
|
||||
/>
|
||||
{isSelected && showThumbnails ? (
|
||||
<SliceAddedBadgePlaceholder
|
||||
placeholderRef={setSliceAddedBadge}
|
||||
showThumbnails={showThumbnails}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
css={css`
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
data-test="card-title"
|
||||
css={(theme: Theme) => css`
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
font-weight: ${theme.typography.weights.bold};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<TruncatedTextWithTooltip>{sliceName}</TruncatedTextWithTooltip>
|
||||
{isSelected && !showThumbnails ? (
|
||||
<SliceAddedBadgePlaceholder
|
||||
placeholderRef={setSliceAddedBadge}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
<MetadataItem label={t('Viz type')} value={vizName} />
|
||||
<MetadataItem
|
||||
label={t('Dataset')}
|
||||
value={<a href={datasourceUrl}>{datasourceName}</a>}
|
||||
/>
|
||||
<MetadataItem label={t('Modified')} value={lastModified} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SliceAddedBadge placeholder={sliceAddedBadge} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSliceCard;
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* 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 AddSliceCard from './AddSliceCard';
|
||||
|
||||
export default AddSliceCard;
|
|
@ -83,7 +83,7 @@ const DEFAULT_SORT_KEY = 'changed_on';
|
|||
const MARGIN_BOTTOM = 16;
|
||||
const SIDEPANE_HEADER_HEIGHT = 30;
|
||||
const SLICE_ADDER_CONTROL_HEIGHT = 64;
|
||||
const DEFAULT_CELL_HEIGHT = 112;
|
||||
const DEFAULT_CELL_HEIGHT = 128;
|
||||
|
||||
const Controls = styled.div`
|
||||
display: flex;
|
||||
|
@ -273,6 +273,7 @@ class SliceAdder extends React.Component {
|
|||
visType={cellData.viz_type}
|
||||
datasourceUrl={cellData.datasource_url}
|
||||
datasourceName={cellData.datasource_name}
|
||||
thumbnailUrl={cellData.thumbnail_url}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
)}
|
||||
|
|
Loading…
Reference in New Issue