diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.js b/superset-frontend/src/dashboard/actions/sliceEntities.js index f0f28ffefb..74fd8fd7f2 100644 --- a/superset-frontend/src/dashboard/actions/sliceEntities.js +++ b/superset-frontend/src/dashboard/actions/sliceEntities.js @@ -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, }; }); diff --git a/superset-frontend/src/dashboard/components/AddSliceCard.jsx b/superset-frontend/src/dashboard/components/AddSliceCard.jsx deleted file mode 100644 index 7a8f7f3b78..0000000000 --- a/superset-frontend/src/dashboard/components/AddSliceCard.jsx +++ /dev/null @@ -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 ( - -
-
- {sliceName} -
-
-
- {t('Modified')} - {lastModified} -
-
- {t('Visualization')} - {visType} -
-
- {t('Data source')} - {datasourceName} -
-
-
- {isSelected &&
{t('Added')}
} -
- ); -} - -AddSliceCard.propTypes = propTypes; -AddSliceCard.defaultProps = defaultProps; - -export default AddSliceCard; diff --git a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.test.tsx b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.test.tsx new file mode 100644 index 0000000000..26cd7b945d --- /dev/null +++ b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.test.tsx @@ -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; +}; + +test('do not render thumbnail if feature flag is not set', async () => { + global.featureFlags = { + [FeatureFlag.THUMBNAILS]: false, + }; + + await act(async () => { + render(); + }); + + expect(screen.queryByTestId('thumbnail')).not.toBeInTheDocument(); +}); + +test('render thumbnail if feature flag is set', async () => { + global.featureFlags = { + [FeatureFlag.THUMBNAILS]: true, + }; + + await act(async () => { + render(); + }); + + expect(screen.queryByTestId('thumbnail')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx new file mode 100644 index 0000000000..c87fbf89bb --- /dev/null +++ b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx @@ -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(null); + useEffect(() => { + setIsTruncated( + ref.current ? ref.current.offsetWidth < ref.current.scrollWidth : false, + ); + }, [children]); + + const div = ( +
+ {children} +
+ ); + + return isTruncated ? {div} : div; +}; + +const MetadataItem: React.FC<{ + label: ReactNode; + value: ReactNode; +}> = ({ label, value }) => ( +
css` + font-size: ${theme.typography.sizes.s}px; + display: flex; + justify-content: space-between; + + &:not(:last-child) { + margin-bottom: ${theme.gridUnit}px; + } + `} + > + css` + margin-right: ${theme.gridUnit * 4}px; + color: ${theme.colors.grayscale.base}; + `} + > + {label} + + + {value} + +
+); + +const SliceAddedBadgePlaceholder: React.FC<{ + showThumbnails?: boolean; + placeholderRef: (element: HTMLDivElement) => void; +}> = ({ showThumbnails, placeholderRef }) => ( +
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')} +
+); + +const SliceAddedBadge: React.FC<{ placeholder?: HTMLDivElement }> = ({ + placeholder, +}) => ( +
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')} +
+); + +const AddSliceCard: React.FC<{ + datasourceUrl?: string; + datasourceName?: string; + innerRef?: React.RefObject; + 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(); + const { mountedPluginMetadata } = usePluginContext(); + const vizName = useMemo( + () => mountedPluginMetadata[visType].name, + [mountedPluginMetadata, visType], + ); + + return ( +
+
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'}; + `} + > +
+ {showThumbnails ? ( +
+ + {isSelected && showThumbnails ? ( + + ) : null} +
+ ) : null} +
+
css` + margin-bottom: ${theme.gridUnit * 2}px; + font-weight: ${theme.typography.weights.bold}; + display: flex; + justify-content: space-between; + align-items: center; + `} + > + {sliceName} + {isSelected && !showThumbnails ? ( + + ) : null} +
+
+ + {datasourceName}} + /> + +
+
+
+
+ +
+ ); +}; + +export default AddSliceCard; diff --git a/superset-frontend/src/dashboard/components/AddSliceCard/index.ts b/superset-frontend/src/dashboard/components/AddSliceCard/index.ts new file mode 100644 index 0000000000..c3736da122 --- /dev/null +++ b/superset-frontend/src/dashboard/components/AddSliceCard/index.ts @@ -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; diff --git a/superset-frontend/src/dashboard/components/SliceAdder.jsx b/superset-frontend/src/dashboard/components/SliceAdder.jsx index 95f9180a33..0766bd11f1 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.jsx @@ -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} /> )}