You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by su...@apache.org on 2021/06/26 00:04:18 UTC
[superset] 03/03: get search working
This is an automated email from the ASF dual-hosted git repository.
suddjian pushed a commit to branch viz-metadata-search
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 1798bcd257efa8292b924d248fd06bd80f263707
Author: David Aaron Suddjian <aa...@gmail.com>
AuthorDate: Fri Jun 25 17:03:18 2021 -0700
get search working
---
.../components/controls/VizTypeControl/index.tsx | 323 ++++++++++++++-------
1 file changed, 213 insertions(+), 110 deletions(-)
diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx
index 241507c..a49e1df 100644
--- a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx
@@ -19,12 +19,11 @@
import React, {
ChangeEventHandler,
useCallback,
- useEffect,
- useRef,
+ useMemo,
useState,
} from 'react';
import PropTypes from 'prop-types';
-import { Input, Row, Col } from 'src/common/components';
+import Fuse from 'fuse.js';
import {
t,
getChartMetadataRegistry,
@@ -34,11 +33,13 @@ import {
SupersetTheme,
useTheme,
} from '@superset-ui/core';
-import { useDynamicPluginContext } from 'src/components/DynamicPlugins';
+import { Input } from 'src/common/components';
+import { usePluginContext } from 'src/components/DynamicPlugins';
import Modal from 'src/components/Modal';
import Tabs from 'src/components/Tabs';
import { Tooltip } from 'src/components/Tooltip';
import Label, { Type } from 'src/components/Label';
+import Icons from 'src/components/Icons';
import ControlHeader from 'src/explore/components/ControlHeader';
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
import './VizTypeControl.less';
@@ -126,7 +127,7 @@ const typesWithDefaultOrder = new Set(DEFAULT_ORDER);
export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control';
function VizSupportValidation({ vizType }: { vizType: string }) {
- const state = useDynamicPluginContext();
+ const state = usePluginContext();
if (state.loading || metadataRegistry.has(vizType)) {
return null;
}
@@ -157,9 +158,11 @@ const SectionTitle = styled.h3`
line-height: ${({ theme }) => theme.gridUnit * 6}px;
`;
-const IconPane = styled(Row)`
+const IconPane = styled.div`
overflow: auto;
- padding: ${({ theme }) => theme.gridUnit * 4}px;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
`;
const CategoriesTabs = styled(Tabs)`
@@ -204,6 +207,10 @@ const CategoriesTabs = styled(Tabs)`
}
}
}
+
+ &.ant-tabs-left > .ant-tabs-content-holder > .ant-tabs-content > .ant-tabs-tabpane {
+ padding: ${theme.gridUnit * 2}px;
+ }
`}
`;
@@ -224,8 +231,14 @@ const SearchPane = styled.div`
padding: ${({ theme }) => theme.gridUnit * 4}px;
`;
+const SearchResultsPane = styled.div`
+ padding: ${({ theme }) => theme.gridUnit * 4}px;
+`;
+
const thumbnailContainerCss = (theme: SupersetTheme) => css`
cursor: pointer;
+ width: ${theme.gridUnit * 24}px;
+ margin: ${theme.gridUnit * 2}px;
img {
border: 1px solid ${theme.colors.grayscale.light2};
@@ -242,109 +255,174 @@ const thumbnailContainerCss = (theme: SupersetTheme) => css`
}
`;
+function vizSortFactor(entry: VizEntry) {
+ if (typesWithDefaultOrder.has(entry.key)) {
+ return DEFAULT_ORDER.indexOf(entry.key);
+ }
+ return DEFAULT_ORDER.length;
+}
+
+interface ThumbnailProps {
+ entry: VizEntry;
+ selectedViz: string;
+ setSelectedViz: (viz: string) => void;
+}
+
+const Thumbnail: React.FC<ThumbnailProps> = ({
+ entry,
+ selectedViz,
+ setSelectedViz,
+}) => {
+ const theme = useTheme();
+ const { key, value: type } = entry;
+ const isSelected = selectedViz === entry.key;
+
+ return (
+ <div
+ role="button"
+ // using css instead of a styled component to preserve
+ // the data-test attribute
+ css={thumbnailContainerCss(theme)}
+ tabIndex={0}
+ className={isSelected ? 'selected' : ''}
+ onClick={() => setSelectedViz(key)}
+ data-test="viztype-selector-container"
+ >
+ <img
+ alt={type.name}
+ width="100%"
+ className={`viztype-selector ${isSelected ? 'selected' : ''}`}
+ src={type.thumbnail}
+ />
+ <div
+ className="viztype-label"
+ data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__viztype-label`}
+ >
+ {type.name}
+ </div>
+ </div>
+ );
+};
+
+interface ThumbnailGalleryProps {
+ vizEntries: VizEntry[];
+ selectedViz: string;
+ setSelectedViz: (viz: string) => void;
+}
+
+/** A list of viz thumbnails, used within the viz picker modal */
+const ThumbnailGallery: React.FC<ThumbnailGalleryProps> = ({
+ vizEntries,
+ ...others
+}) => (
+ <IconPane data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__viz-row`}>
+ {vizEntries.map(entry => (
+ <Thumbnail {...others} entry={entry} />
+ ))}
+ </IconPane>
+);
+
+/** Manages the viz type and the viz picker modal */
const VizTypeControl = (props: VizTypeControlProps) => {
- const { value: initialValue, onChange, isModalOpenInit } = props;
+ const { value: initialValue, onChange, isModalOpenInit, labelType } = props;
const theme = useTheme();
+ const { mountedPluginMetadata } = usePluginContext();
const [showModal, setShowModal] = useState(!!isModalOpenInit);
- const [filter, setFilter] = useState('');
- const searchRef = useRef<any>(null);
+ const [activeCategory, setActiveCategory] = useState<string | undefined>(
+ undefined,
+ );
+ const [categoryRecall, setCategoryRecall] = useState<string | undefined>(
+ undefined,
+ );
+ const [searchInputValue, setSearchInputValue] = useState('');
+ const [isSearching, setIsSearching] = useState(false);
const [selectedViz, setSelectedViz] = useState(initialValue);
- useEffect(() => {
- if (showModal) {
- setTimeout(() => searchRef.current?.focus(), 200);
- }
- }, [showModal]);
-
const onSubmit = useCallback(() => {
onChange(selectedViz);
setShowModal(false);
}, [selectedViz, onChange]);
- const toggleModal = () => {
+ const toggleModal = useCallback(() => {
setShowModal(prevState => !prevState);
- };
- const changeSearch: ChangeEventHandler<HTMLInputElement> = event => {
- setFilter(event.target.value);
- };
+ // make sure the modal opens up to the last submitted viz
+ setSelectedViz(initialValue);
+ setActiveCategory(
+ mountedPluginMetadata[initialValue]?.category || undefined,
+ );
+ }, [initialValue, mountedPluginMetadata]);
- const renderItem = (entry: VizEntry) => {
- const { key, value: type } = entry;
- const isSelected = key === selectedViz;
+ const changeSearch: ChangeEventHandler<HTMLInputElement> = useCallback(
+ event => {
+ setSearchInputValue(event.target.value);
+ },
+ [],
+ );
- return (
- <div
- role="button"
- // using css instead of a styled component to preserve
- // the data-test attribute
- css={thumbnailContainerCss(theme)}
- tabIndex={0}
- className={isSelected ? 'selected' : ''}
- onClick={() => setSelectedViz(key)}
- data-test="viztype-selector-container"
- >
- <img
- alt={type.name}
- width="100%"
- className={`viztype-selector ${isSelected ? 'selected' : ''}`}
- src={type.thumbnail}
- />
- <div
- className="viztype-label"
- data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__viztype-label`}
- >
- {type.name}
- </div>
- </div>
- );
- };
-
- const { labelType } = props;
- const filterString = filter.toLowerCase();
- const filterStringParts = filterString.split(' ');
-
- const categories = DEFAULT_ORDER.filter(
- type =>
- metadataRegistry.has(type) &&
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- nativeFilterGate(metadataRegistry.get(type)!.behaviors || []),
- )
- .map(type => ({
- key: type,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- value: metadataRegistry.get(type),
- }))
- .concat(
- metadataRegistry
- .entries()
- .filter(entry => {
- const behaviors = entry.value?.behaviors || [];
- return nativeFilterGate(behaviors);
- })
- .filter(({ key }) => !typesWithDefaultOrder.has(key)),
- )
- .filter(
- entry =>
- !!entry.value &&
- filterStringParts.every(
- part => entry.value?.name.toLowerCase().indexOf(part) !== -1,
- ),
- )
- .reduce((acc, entry) => {
- const category = entry.value?.category || 'Other';
- if (!acc[category]) {
- acc[category] = [];
+ const chartMetadata: VizEntry[] = useMemo(() => {
+ const result = Object.entries(mountedPluginMetadata)
+ .map(([key, value]) => ({ key, value }))
+ .filter(({ value }) => nativeFilterGate(value.behaviors || []));
+ result.sort((a, b) => vizSortFactor(a) - vizSortFactor(b));
+ return result;
+ }, [mountedPluginMetadata]);
+
+ const chartsByCategory = useMemo(() => {
+ const result: Record<string, VizEntry[]> = {};
+ chartMetadata.forEach(entry => {
+ const category = entry.value.category || 'Other';
+ if (!result[category]) {
+ result[category] = [];
}
- // typecast is safe because we filtered out falsy value already
- acc[category].push(entry as VizEntry);
- return acc;
- }, {} as Record<string, VizEntry[]>);
+ result[category].push(entry);
+ });
+ return result;
+ }, [chartMetadata]);
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(chartMetadata, {
+ ignoreLocation: true,
+ threshold: 0.3,
+ keys: ['value.name', 'value.tags', 'value.description'],
+ }),
+ [chartMetadata],
+ );
+
+ const searchResults = useMemo(() => {
+ if (searchInputValue.trim() === '') {
+ return [];
+ }
+ return fuse.search(searchInputValue).map(result => result.item);
+ }, [searchInputValue, fuse]);
+
+ const startSearching = useCallback(() => {
+ if (activeCategory !== undefined) {
+ // this will allow us to go back to the tab the user
+ // was looking at when they exit their search.
+ // "undefined" check is needed because undefined is used when searching,
+ // and we don't want to go back to that when we stop searching.
+ setCategoryRecall(activeCategory);
+ }
+ setActiveCategory(undefined);
+ setIsSearching(true);
+ }, [activeCategory]);
+
+ const stopSearching = useCallback(() => {
+ setActiveCategory(categoryRecall);
+ setIsSearching(false);
+ }, [categoryRecall]);
+
+ const onTabClick = useCallback((key: string) => {
+ setActiveCategory(key);
+ setIsSearching(false);
+ }, []);
const labelContent =
- metadataRegistry.get(initialValue)?.name || `${initialValue}`;
+ mountedPluginMetadata[initialValue]?.name || `${initialValue}`;
- const selectedVizMetadata = metadataRegistry.get(selectedViz);
+ const selectedVizMetadata = mountedPluginMetadata[selectedViz];
return (
<div>
@@ -365,6 +443,7 @@ const VizTypeControl = (props: VizTypeControlProps) => {
<VizSupportValidation vizType={initialValue} />
</>
</Tooltip>
+
<UnpaddedModal
show={showModal}
onHide={toggleModal}
@@ -376,30 +455,54 @@ const VizTypeControl = (props: VizTypeControlProps) => {
<VizPickerLayout>
<SearchPane>
<Input
- ref={searchRef}
type="text"
- value={filter}
+ value={searchInputValue}
placeholder={t('Search')}
onChange={changeSearch}
+ onFocus={startSearching}
data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__search-input`}
+ suffix={
+ <div
+ css={css`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: ${theme.colors.grayscale.base};
+ `}
+ >
+ <Icons.XLarge iconSize="m" onClick={stopSearching} />
+ </div>
+ }
/>
</SearchPane>
- <CategoriesTabs tabPosition="left">
- {Object.entries(categories).map(([category, vizTypes]) => (
- <Tabs.TabPane tab={category} key={category}>
- <IconPane
- data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__viz-row`}
- gutter={16}
- >
- {vizTypes.map(entry => (
- <Col xs={12} sm={8} md={6} lg={4} key={entry.key}>
- {renderItem(entry)}
- </Col>
- ))}
- </IconPane>
- </Tabs.TabPane>
- ))}
- </CategoriesTabs>
+
+ {isSearching ? (
+ <SearchResultsPane>
+ {/* <h3 css={searchResultsHeaderCss}>Search Results</h3> */}
+ <ThumbnailGallery
+ vizEntries={searchResults}
+ selectedViz={selectedViz}
+ setSelectedViz={setSelectedViz}
+ />
+ </SearchResultsPane>
+ ) : (
+ <CategoriesTabs
+ tabPosition="left"
+ activeKey={activeCategory}
+ onTabClick={onTabClick}
+ >
+ {Object.entries(chartsByCategory).map(([category, vizTypes]) => (
+ <Tabs.TabPane tab={category} key={category}>
+ <ThumbnailGallery
+ vizEntries={vizTypes}
+ selectedViz={selectedViz}
+ setSelectedViz={setSelectedViz}
+ />
+ </Tabs.TabPane>
+ ))}
+ </CategoriesTabs>
+ )}
+
<DetailsPane>
<SectionTitle>{selectedVizMetadata?.name}</SectionTitle>
<Description>