This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new d50784dd80 feat(dashboard): Add thumbnails to dashboard edit draggable 
chart list (#20528)
d50784dd80 is described below

commit d50784dd808cf908567e2c7f9fa67188202c59b9
Author: Cody Leff <[email protected]>
AuthorDate: Thu Jul 28 12:46:13 2022 -0400

    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.
---
 .../src/dashboard/actions/sliceEntities.js         |   2 +
 .../src/dashboard/components/AddSliceCard.jsx      | 148 -----------
 .../components/AddSliceCard/AddSliceCard.test.tsx  |  62 +++++
 .../components/AddSliceCard/AddSliceCard.tsx       | 279 +++++++++++++++++++++
 .../src/dashboard/components/AddSliceCard/index.ts |  22 ++
 .../src/dashboard/components/SliceAdder.jsx        |   3 +-
 6 files changed, 367 insertions(+), 149 deletions(-)

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 (
-    <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;
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<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();
+});
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<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;
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}
           />
         )}

Reply via email to