diff --git a/superset-frontend/images/icons/ballot.svg b/superset-frontend/images/icons/ballot.svg
new file mode 100644
index 00000000000..00d1a235ce1
--- /dev/null
+++ b/superset-frontend/images/icons/ballot.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/superset-frontend/images/icons/category.svg b/superset-frontend/images/icons/category.svg
new file mode 100644
index 00000000000..471cf254d02
--- /dev/null
+++ b/superset-frontend/images/icons/category.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/superset-frontend/images/icons/tags.svg b/superset-frontend/images/icons/tags.svg
new file mode 100644
index 00000000000..36a061ecf33
--- /dev/null
+++ b/superset-frontend/images/icons/tags.svg
@@ -0,0 +1,22 @@
+
+
+
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index c4554977fad..4314db65e2d 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -67596,7 +67596,8 @@
"compute-scroll-into-view": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz",
- "integrity": "sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ=="
+ "integrity": "sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ==",
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
@@ -87305,11 +87306,18 @@
}
},
"scroll-into-view-if-needed": {
- "version": "2.2.26",
- "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.26.tgz",
- "integrity": "sha512-SQ6AOKfABaSchokAmmaxVnL9IArxEnLEX9j4wAZw+x4iUTb40q7irtHG3z4GtAWz5veVZcCnubXDBRyLVQaohw==",
+ "version": "2.2.28",
+ "resolved": "https://registry.npm.taobao.org/scroll-into-view-if-needed/download/scroll-into-view-if-needed-2.2.28.tgz",
+ "integrity": "sha1-WhWy9YpSZCyIyOylhGROAXA9ZFo=",
"requires": {
- "compute-scroll-into-view": "^1.0.16"
+ "compute-scroll-into-view": "^1.0.17"
+ },
+ "dependencies": {
+ "compute-scroll-into-view": {
+ "version": "1.0.17",
+ "resolved": "https://registry.npm.taobao.org/compute-scroll-into-view/download/compute-scroll-into-view-1.0.17.tgz?cache=0&sync_timestamp=1614042424875&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcompute-scroll-into-view%2Fdownload%2Fcompute-scroll-into-view-1.0.17.tgz",
+ "integrity": "sha1-aojxis2dQunPS6pr7H4FImB6t6s="
+ }
}
},
"seedrandom": {
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index de78593199c..18d791fd25f 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -179,6 +179,7 @@
"redux-undo": "^1.0.0-beta9-9-7",
"regenerator-runtime": "^0.13.5",
"rison": "^0.1.1",
+ "scroll-into-view-if-needed": "^2.2.28",
"shortid": "^2.2.6",
"urijs": "^1.19.6",
"use-immer": "^0.4.2",
diff --git a/superset-frontend/spec/javascripts/explore/components/VizTypeControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/VizTypeControl_spec.jsx
index 1258b808528..02cbdac19c5 100644
--- a/superset-frontend/spec/javascripts/explore/components/VizTypeControl_spec.jsx
+++ b/superset-frontend/spec/javascripts/explore/components/VizTypeControl_spec.jsx
@@ -84,7 +84,7 @@ describe('VizTypeControl', () => {
const thumbnails = screen.getByTestId('viztype-selector-container');
expect(thumbnails).toBeInTheDocument();
- const searchInput = screen.getByPlaceholderText('Search');
+ const searchInput = screen.getByPlaceholderText('Search all charts');
userEvent.type(searchInput, 'foo');
await waitForEffects();
diff --git a/superset-frontend/src/components/Icons/index.tsx b/superset-frontend/src/components/Icons/index.tsx
index 6eed2e8f7f0..f2b19445f7a 100644
--- a/superset-frontend/src/components/Icons/index.tsx
+++ b/superset-frontend/src/components/Icons/index.tsx
@@ -150,6 +150,9 @@ const IconFileNames = [
'warning_solid',
'x-large',
'x-small',
+ 'tags',
+ 'ballot',
+ 'category',
];
const iconOverrides: Record = {};
diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx
index a8534ccb497..cf2a200a262 100644
--- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx
+++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx
@@ -105,7 +105,9 @@ describe('VizTypeControl', () => {
const visualizations = screen.getByTestId(getTestId('viz-row'));
- userEvent.click(screen.getByRole('button', { name: 'Table' }));
+ userEvent.click(
+ screen.getByRole('button', { name: 'category Table close' }),
+ );
expect(visualizations).toHaveTextContent(/Time-series Table/);
diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
index 746bda85e04..805f8cb8479 100644
--- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
+++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
@@ -19,6 +19,7 @@
import React, {
ChangeEventHandler,
useCallback,
+ useEffect,
useMemo,
useRef,
useState,
@@ -32,11 +33,13 @@ import {
SupersetTheme,
useTheme,
} from '@superset-ui/core';
-import { Input } from 'src/common/components';
+import { Collapse, Input } from 'src/common/components';
import Label from 'src/components/Label';
import { usePluginContext } from 'src/components/DynamicPlugins';
import Icons from 'src/components/Icons';
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
+import { CloseOutlined } from '@ant-design/icons';
+import scrollIntoView from 'scroll-into-view-if-needed';
interface VizTypeGalleryProps {
onChange: (vizType: string | null) => void;
@@ -96,8 +99,6 @@ const DEFAULT_ORDER = [
'country_map',
];
-const ALL_TAGS = [t('Highly-used'), t('Text'), t('Trend'), t('Formattable')];
-
const typesWithDefaultOrder = new Set(DEFAULT_ORDER);
const THUMBNAIL_GRID_UNITS = 24;
@@ -106,16 +107,22 @@ export const MAX_ADVISABLE_VIZ_GALLERY_WIDTH = 1090;
const OTHER_CATEGORY = t('Other');
-const DEFAULT_SEARCH_INPUT_VALUE = t('Highly-used');
+const ALL_CHARTS = t('All charts');
+
+const RECOMMENDED_TAGS = [
+ t('Highly-used'),
+ t('ECharts'),
+ t('Advanced-Analytics'),
+];
export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control';
const VizPickerLayout = styled.div`
display: grid;
grid-template-rows: auto minmax(100px, 1fr) minmax(200px, 35%);
- grid-template-columns: 1fr 5fr;
+ grid-template-columns: auto 5fr;
grid-template-areas:
- 'sidebar tags'
+ 'sidebar search'
'sidebar main'
'details details';
height: 70vh;
@@ -135,8 +142,21 @@ const LeftPane = styled.div`
display: flex;
flex-direction: column;
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
- padding: ${({ theme }) => theme.gridUnit * 2}px;
- overflow: hidden;
+ overflow: auto;
+
+ .ant-collapse .ant-collapse-item {
+ .ant-collapse-header {
+ font-size: ${({ theme }) => theme.typography.sizes.s}px;
+ color: ${({ theme }) => theme.colors.grayscale.base};
+ padding-left: ${({ theme }) => theme.gridUnit * 2}px;
+ padding-bottom: ${({ theme }) => theme.gridUnit}px;
+ }
+ .ant-collapse-content .ant-collapse-content-box {
+ display: flex;
+ flex-direction: column;
+ padding: 0 ${({ theme }) => theme.gridUnit * 2}px;
+ }
+ }
`;
const RightPane = styled.div`
@@ -144,25 +164,10 @@ const RightPane = styled.div`
overflow-y: scroll;
`;
-const AllTagsWrapper = styled.div`
- ${({ theme }) => `
- grid-area: tags;
- margin: ${theme.gridUnit * 4}px ${theme.gridUnit * 2}px 0;
- input {
- font-size: ${theme.typography.sizes.s};
- }
- `}
-`;
-
-const CategoriesWrapper = styled.div`
- display: flex;
- flex-direction: column;
- overflow: auto;
-`;
-
const SearchWrapper = styled.div`
${({ theme }) => `
- margin-bottom: ${theme.gridUnit * 2}px;
+ grid-area: search;
+ margin: ${theme.gridUnit * 2}px ${theme.gridUnit * 3}px;
input {
font-size: ${theme.typography.sizes.s};
}
@@ -180,21 +185,48 @@ const InputIconAlignment = styled.div`
color: ${({ theme }) => theme.colors.grayscale.base};
`;
-const CategoryLabel = styled.button`
+const SelectorLabel = styled.button`
${({ theme }) => `
all: unset; // remove default button styles
cursor: pointer;
- padding: ${theme.gridUnit}px;
+ margin: ${theme.gridUnit}px 0;
+ padding: 0 ${theme.gridUnit * 6}px 0 ${theme.gridUnit}px;
border-radius: ${theme.borderRadius}px;
line-height: 2em;
font-size: ${theme.typography.sizes.s};
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ position: relative;
&:focus {
outline: initial;
}
&.selected {
- background-color: ${theme.colors.secondary.light4};
+ background-color: ${theme.colors.primary.dark1};
+ color: ${theme.colors.primary.light5};
+
+ svg {
+ color: ${theme.colors.primary.light5};
+ }
+
+ &:hover {
+ .cancel {
+ visibility: visible;
+ }
+ }
+ }
+
+ svg {
+ position: relative;
+ top: ${theme.gridUnit * 2}px
+ }
+
+ .cancel {
+ visibility: hidden;
+ position: absolute;
+ right: ${theme.gridUnit * 2}px;
}
`}
`;
@@ -362,32 +394,54 @@ const ThumbnailGallery: React.FC = ({
);
-const CategorySelector: React.FC<{
- category: string;
+const Selector: React.FC<{
+ selector: string;
+ icon: JSX.Element;
isSelected: boolean;
- onClick: (category: string) => void;
-}> = ({ category, isSelected, onClick }) => (
- onClick(category)}
- >
- {category}
-
-);
+ onClick: (selector: string) => void;
+ onClear: (e: React.MouseEvent) => void;
+}> = ({ selector, icon, isSelected, onClick, onClear }) => {
+ const btnRef = useRef(null);
-const doesVizMatchCategory = (viz: ChartMetadata, category: string) =>
- category === viz.category ||
- (category === OTHER_CATEGORY && viz.category == null);
+ // see Element.scrollIntoViewIfNeeded()
+ // see: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded
+ useEffect(() => {
+ if (isSelected) {
+ // We need to wait for the modal to open and then scroll, so we put it in the microtask queue
+ queueMicrotask(() =>
+ scrollIntoView(btnRef.current as HTMLButtonElement, {
+ behavior: 'smooth',
+ scrollMode: 'if-needed',
+ }),
+ );
+ }
+ }, []);
+
+ return (
+ onClick(selector)}
+ >
+ {icon}
+ {selector}
+
+
+ );
+};
+
+const doesVizMatchSelector = (viz: ChartMetadata, selector: string) =>
+ selector === viz.category ||
+ (selector === OTHER_CATEGORY && viz.category == null) ||
+ (viz.tags || []).indexOf(selector) > -1;
export default function VizTypeGallery(props: VizTypeGalleryProps) {
const { selectedViz, onChange, className } = props;
const { mountedPluginMetadata } = usePluginContext();
const searchInputRef = useRef();
- const [searchInputValue, setSearchInputValue] = useState(
- DEFAULT_SEARCH_INPUT_VALUE,
- );
+ const [searchInputValue, setSearchInputValue] = useState('');
const [isSearchFocused, setIsSearchFocused] = useState(true);
const isActivelySearching = isSearchFocused && !!searchInputValue;
@@ -430,8 +484,38 @@ export default function VizTypeGallery(props: VizTypeGalleryProps) {
[chartsByCategory],
);
- const [activeCategory, setActiveCategory] = useState(
- () => selectedVizMetadata?.category || categories[0],
+ const chartsByTags = useMemo(() => {
+ const result: Record = {};
+ chartMetadata.forEach(entry => {
+ const tags = entry.value.tags || [];
+ tags.forEach(tag => {
+ if (!result[tag]) {
+ result[tag] = [];
+ }
+ result[tag].push(entry);
+ });
+ });
+ return result;
+ }, [chartMetadata]);
+
+ const tags = useMemo(
+ () =>
+ Object.keys(chartsByTags)
+ .sort((a, b) =>
+ // sort alphabetically
+ a.localeCompare(b),
+ )
+ .filter(tag => RECOMMENDED_TAGS.indexOf(tag) === -1),
+ [chartsByCategory],
+ );
+
+ const sortedMetadata = useMemo(
+ () => chartMetadata.sort((a, b) => a.key.localeCompare(b.key)),
+ [chartMetadata],
+ );
+
+ const [activeSelector, setActiveSelector] = useState(
+ () => selectedVizMetadata?.category || RECOMMENDED_TAGS[0],
);
// get a fuse instance for fuzzy search
@@ -472,83 +556,125 @@ export default function VizTypeGallery(props: VizTypeGalleryProps) {
searchInputRef.current!.blur();
}, []);
- const selectCategory = useCallback(
+ const clickSelector = useCallback(
(key: string) => {
if (isSearchFocused) {
stopSearching();
}
- setActiveCategory(key);
- // clear the selected viz if it is not present in the new category
+ setActiveSelector(key);
+ // clear the selected viz if it is not present in the new category or tags
const isSelectedVizCompatible =
- selectedVizMetadata && doesVizMatchCategory(selectedVizMetadata, key);
- if (key !== activeCategory && !isSelectedVizCompatible) {
+ selectedVizMetadata && doesVizMatchSelector(selectedVizMetadata, key);
+ if (key !== activeSelector && !isSelectedVizCompatible) {
onChange(null);
}
},
[
stopSearching,
isSearchFocused,
- activeCategory,
+ activeSelector,
selectedVizMetadata,
onChange,
],
);
+ const clearSelector = useCallback(e => {
+ e.stopPropagation();
+ if (isSearchFocused) {
+ stopSearching();
+ }
+ // clear current selector and set all charts
+ setActiveSelector(ALL_CHARTS);
+ }, []);
+
+ const sectionMap = useMemo(
+ () => ({
+ RECOMMENDED_TAGS: {
+ title: t('Recommended tags'),
+ icon: ,
+ selectors: RECOMMENDED_TAGS,
+ },
+ ALL: {
+ title: t('All'),
+ icon: ,
+ selectors: [ALL_CHARTS],
+ },
+ CATEGORY: {
+ title: t('Category'),
+ icon: ,
+ selectors: categories,
+ },
+ TAGS: {
+ title: t('Tags'),
+ icon: ,
+ selectors: tags,
+ },
+ }),
+ [categories, tags],
+ );
+
const vizEntriesToDisplay = isActivelySearching
? searchResults
- : chartsByCategory[activeCategory] || [];
+ : activeSelector === ALL_CHARTS
+ ? sortedMetadata
+ : chartsByCategory[activeSelector] || chartsByTags[activeSelector] || [];
return (
-
-
-
-
- }
- suffix={
-
- {searchInputValue && (
-
- )}
-
- }
- />
-
-
- {categories.map(category => (
-
- ))}
-
+
+ {Object.keys(sectionMap).map(key => {
+ const section = sectionMap[key];
+
+ return (
+ {section.title}}
+ key={key}
+ >
+ {section.selectors.map((selector: string) => (
+
+ ))}
+
+ );
+ })}
+
-
- {ALL_TAGS.map(tag => (
-
- ))}
-
+
+
+
+
+ }
+ suffix={
+
+ {searchInputValue && (
+
+ )}
+
+ }
+ />
+