This is an automated email from the ASF dual-hosted git repository. suddjian pushed a commit to branch viz-gallery-component in repository https://gitbox.apache.org/repos/asf/superset.git
commit dccb33c9b9cf42139221b5f888b4c2f30a2406cb Author: David Aaron Suddjian <aasuddj...@gmail.com> AuthorDate: Thu Jul 1 01:42:32 2021 -0700 separate viz gallery from the modal --- .../{index.tsx => VizTypeGallery.tsx} | 257 ++++-------- .../components/controls/VizTypeControl/index.tsx | 448 +-------------------- 2 files changed, 76 insertions(+), 629 deletions(-) diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx similarity index 61% copy from superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx copy to superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx index ec3ce7b..4649de2 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx @@ -1,21 +1,3 @@ -/** - * 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, { ChangeEventHandler, useCallback, @@ -43,23 +25,9 @@ import ControlHeader from 'src/explore/components/ControlHeader'; import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils'; import './VizTypeControl.less'; -const propTypes = { - description: PropTypes.string, - label: PropTypes.string, - name: PropTypes.string.isRequired, - onChange: PropTypes.func, - value: PropTypes.string.isRequired, - labelType: PropTypes.string, -}; - -interface VizTypeControlProps { - description?: string; - label?: string; - name: string; +interface VizTypeGalleryProps { onChange: (vizType: string) => void; value: string; - labelType?: Type; - isModalOpenInit?: boolean; } type VizEntry = { @@ -67,13 +35,6 @@ type VizEntry = { value: ChartMetadata; }; -const defaultProps = { - onChange: () => {}, - labelType: 'default', -}; - -const metadataRegistry = getChartMetadataRegistry(); - const DEFAULT_ORDER = [ 'line', 'big_number', @@ -127,25 +88,6 @@ const THUMBNAIL_GRID_UNITS = 24; export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control'; -function VizSupportValidation({ vizType }: { vizType: string }) { - const state = usePluginContext(); - if (state.loading || metadataRegistry.has(vizType)) { - return null; - } - return ( - <div className="text-danger"> - <i className="fa fa-exclamation-circle text-danger" />{' '} - <small>{t('This visualization type is not supported.')}</small> - </div> - ); -} - -const UnpaddedModal = styled(Modal)` - .ant-modal-body { - padding: 0; - } -`; - const VizPickerLayout = styled.div` display: grid; grid-template-rows: 1fr 35%; @@ -361,19 +303,11 @@ const CategorySelector: React.FC<{ </CategoryLabel> ); -/** Manages the viz type and the viz picker modal */ -const VizTypeControl = (props: VizTypeControlProps) => { - const { value: initialValue, onChange, isModalOpenInit, labelType } = props; +export default function VizTypeGallery(props: VizTypeGalleryProps) { + const { value: selectedViz, onChange } = props; const { mountedPluginMetadata } = usePluginContext(); - const [showModal, setShowModal] = useState(!!isModalOpenInit); const [searchInputValue, setSearchInputValue] = useState(''); const [isSearching, setIsSearching] = useState(false); - const [selectedViz, setSelectedViz] = useState(initialValue); - - const onSubmit = useCallback(() => { - onChange(selectedViz); - setShowModal(false); - }, [selectedViz, onChange]); const changeSearch: ChangeEventHandler<HTMLInputElement> = useCallback( event => { @@ -445,19 +379,6 @@ const VizTypeControl = (props: VizTypeControlProps) => { [stopSearching], ); - const toggleModal = useCallback(() => { - setShowModal(prevState => !prevState); - - // make sure the modal opens up to the last submitted viz - setSelectedViz(initialValue); - setActiveCategory( - mountedPluginMetadata[initialValue]?.category || categories[0], - ); - }, [initialValue, mountedPluginMetadata, categories]); - - const labelContent = - mountedPluginMetadata[initialValue]?.name || `${initialValue}`; - const selectedVizMetadata: ChartMetadata | undefined = mountedPluginMetadata[selectedViz]; @@ -466,110 +387,74 @@ const VizTypeControl = (props: VizTypeControlProps) => { : chartsByCategory[activeCategory] || []; return ( - <div> - <ControlHeader {...props} /> - <Tooltip - id="error-tooltip" - placement="right" - title={t('Click to change visualization type')} - > - <> - <Label - onClick={toggleModal} - type={labelType} - data-test="visualization-type" - > - {labelContent} - </Label> - <VizSupportValidation vizType={initialValue} /> - </> - </Tooltip> - - <UnpaddedModal - show={showModal} - onHide={toggleModal} - title={t('Select a visualization type')} - primaryButtonName={t('Select')} - onHandledPrimaryAction={onSubmit} - maxWidth="1090px" - responsive - > - <VizPickerLayout> - <LeftPane> - <SearchWrapper> - <Input - type="text" - value={searchInputValue} - placeholder={t('Search')} - onChange={changeSearch} - onFocus={startSearching} - data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__search-input`} - prefix={ - <InputIconAlignment> - <Icons.Search iconSize="m" /> - </InputIconAlignment> - } - suffix={ - <InputIconAlignment> - {searchInputValue && ( - <Icons.XLarge iconSize="m" onClick={stopSearching} /> - )} - </InputIconAlignment> - } - /> - </SearchWrapper> - {categories.map(category => ( - <CategorySelector - key={category} - category={category} - isSelected={!isSearching && category === activeCategory} - onClick={onTabClick} - /> - ))} - </LeftPane> - - <ThumbnailGallery - vizEntries={vizEntriesToDisplay} - selectedViz={selectedViz} - setSelectedViz={setSelectedViz} + <VizPickerLayout> + <LeftPane> + <SearchWrapper> + <Input + type="text" + value={searchInputValue} + placeholder={t('Search')} + onChange={changeSearch} + onFocus={startSearching} + data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__search-input`} + prefix={ + <InputIconAlignment> + <Icons.Search iconSize="m" /> + </InputIconAlignment> + } + suffix={ + <InputIconAlignment> + {searchInputValue && ( + <Icons.XLarge iconSize="m" onClick={stopSearching} /> + )} + </InputIconAlignment> + } /> + </SearchWrapper> + {categories.map(category => ( + <CategorySelector + key={category} + category={category} + isSelected={!isSearching && category === activeCategory} + onClick={onTabClick} + /> + ))} + </LeftPane> - <DetailsPane> - <SectionTitle - css={css` - grid-area: viz-name; - `} - > - {selectedVizMetadata?.name} - </SectionTitle> - <Description> - {selectedVizMetadata?.description || - t('No description available.')} - </Description> - <SectionTitle - css={css` - grid-area: examples-header; - `} - > - {!!selectedVizMetadata?.exampleGallery?.length && t('Examples')} - </SectionTitle> - <Examples> - {(selectedVizMetadata?.exampleGallery || []).map(example => ( - <img - src={example.url} - alt={example.caption} - title={example.caption} - /> - ))} - </Examples> - </DetailsPane> - </VizPickerLayout> - </UnpaddedModal> - </div> - ); -}; - -VizTypeControl.propTypes = propTypes; -VizTypeControl.defaultProps = defaultProps; + <ThumbnailGallery + vizEntries={vizEntriesToDisplay} + selectedViz={selectedViz} + setSelectedViz={onChange} + /> -export default VizTypeControl; + <DetailsPane> + <SectionTitle + css={css` + grid-area: viz-name; + `} + > + {selectedVizMetadata?.name} + </SectionTitle> + <Description> + {selectedVizMetadata?.description || t('No description available.')} + </Description> + <SectionTitle + css={css` + grid-area: examples-header; + `} + > + {!!selectedVizMetadata?.exampleGallery?.length && t('Examples')} + </SectionTitle> + <Examples> + {(selectedVizMetadata?.exampleGallery || []).map(example => ( + <img + src={example.url} + alt={example.caption} + title={example.caption} + /> + ))} + </Examples> + </DetailsPane> + </VizPickerLayout> + ); +} diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx index ec3ce7b..eff1c4d 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx @@ -16,32 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import React, { - ChangeEventHandler, - useCallback, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useState } from 'react'; import PropTypes from 'prop-types'; -import Fuse from 'fuse.js'; -import { - t, - getChartMetadataRegistry, - styled, - css, - ChartMetadata, - SupersetTheme, - useTheme, -} from '@superset-ui/core'; -import { Input } from 'src/common/components'; +import { t, getChartMetadataRegistry, styled } from '@superset-ui/core'; import { usePluginContext } from 'src/components/DynamicPlugins'; import Modal from 'src/components/Modal'; 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'; +import VizTypeGallery from './VizTypeGallery'; const propTypes = { description: PropTypes.string, @@ -62,11 +46,6 @@ interface VizTypeControlProps { isModalOpenInit?: boolean; } -type VizEntry = { - key: string; - value: ChartMetadata; -}; - const defaultProps = { onChange: () => {}, labelType: 'default', @@ -74,57 +53,6 @@ const defaultProps = { const metadataRegistry = getChartMetadataRegistry(); -const DEFAULT_ORDER = [ - 'line', - 'big_number', - 'table', - 'filter_box', - 'dist_bar', - 'area', - 'bar', - 'deck_polygon', - 'pie', - 'time_table', - 'pivot_table', - 'histogram', - 'big_number_total', - 'deck_scatter', - 'deck_hex', - 'time_pivot', - 'deck_arc', - 'heatmap', - 'deck_grid', - 'dual_line', - 'deck_screengrid', - 'line_multi', - 'treemap', - 'box_plot', - 'sunburst', - 'sankey', - 'word_cloud', - 'mapbox', - 'kepler', - 'cal_heatmap', - 'rose', - 'bubble', - 'deck_geojson', - 'horizon', - 'deck_multi', - 'compare', - 'partition', - 'event_flow', - 'deck_path', - 'graph_chart', - 'world_map', - 'paired_ttest', - 'para', - 'country_map', -]; - -const typesWithDefaultOrder = new Set(DEFAULT_ORDER); - -const THUMBNAIL_GRID_UNITS = 24; - export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control'; function VizSupportValidation({ vizType }: { vizType: string }) { @@ -146,228 +74,11 @@ const UnpaddedModal = styled(Modal)` } `; -const VizPickerLayout = styled.div` - display: grid; - grid-template-rows: 1fr 35%; - grid-template-columns: 1fr 5fr; - grid-template-areas: - 'sidebar main' - 'details details'; - height: 70vh; -`; - -const SectionTitle = styled.h3` - margin-top: 0; - margin-bottom: ${({ theme }) => theme.gridUnit * 2}px; - font-size: ${({ theme }) => theme.typography.sizes.l}px; - font-weight: ${({ theme }) => theme.typography.weights.bold}; - line-height: ${({ theme }) => theme.gridUnit * 6}px; -`; - -const LeftPane = styled.div` - grid-area: sidebar; - display: flex; - flex-direction: column; - border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - padding: ${({ theme }) => theme.gridUnit * 2}px; -`; - -const SearchWrapper = styled.div` - ${({ theme }) => ` - margin-bottom: ${theme.gridUnit * 2}px; - input { - font-size: ${theme.typography.sizes.s}; - } - .ant-input-affix-wrapper { - padding-left: ${theme.gridUnit * 2}px; - } - `} -`; - -/** Styles to line up prefix/suffix icons in the search input */ -const InputIconAlignment = styled.div` - display: flex; - justify-content: center; - align-items: center; - color: ${({ theme }) => theme.colors.grayscale.base}; -`; - -const CategoryLabel = styled.button` - ${({ theme }) => ` - all: unset; // remove default button styles - cursor: pointer; - padding: ${theme.gridUnit}px; - border-radius: ${theme.borderRadius}px; - line-height: 2em; - font-size: ${theme.typography.sizes.s}; - - &:focus { - outline: initial; - } - - &.selected { - background-color: ${theme.colors.secondary.light4}; - } - `} -`; - -const IconsPane = styled.div` - grid-area: main; - overflow: auto; - display: flex; - flex-direction: row; - flex-wrap: wrap; - padding: ${({ theme }) => theme.gridUnit * 2}px; -`; - -const DetailsPane = styled.div` - grid-area: details; - border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - padding: ${({ theme }) => theme.gridUnit * 4}px; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: auto 1fr; - grid-template-areas: - 'viz-name examples-header' - 'description examples'; -`; - -// overflow hidden on the details pane and overflow auto on the description -// (plus grid layout) enables the description to scroll while the header stays in place. - -const Description = styled.p` - grid-area: description; - overflow: auto; - padding-right: ${({ theme }) => theme.gridUnit * 14}px; -`; - -const Examples = styled.div` - grid-area: examples; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - overflow: auto; - gap: ${({ theme }) => theme.gridUnit * 4}px; - - img { - height: 100%; - border-radius: ${({ theme }) => theme.gridUnit}px; - border: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - } -`; - -const thumbnailContainerCss = (theme: SupersetTheme) => css` - cursor: pointer; - width: ${theme.gridUnit * THUMBNAIL_GRID_UNITS}px; - margin: ${theme.gridUnit * 2}px; - - img { - border: 1px solid ${theme.colors.grayscale.light2}; - border-radius: ${theme.gridUnit}px; - transition: border-color ${theme.transitionTiming}; - } - - &.selected img { - border: 2px solid ${theme.colors.primary.light2}; - } - - &:hover:not(.selected) img { - border: 1px solid ${theme.colors.grayscale.light1}; - } - - .viztype-label { - text-align: center; - } -`; - -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, - ...props -}) => ( - <IconsPane data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__viz-row`}> - {vizEntries.map(entry => ( - <Thumbnail key={entry.key} {...props} entry={entry} /> - ))} - </IconsPane> -); - -const CategorySelector: React.FC<{ - category: string; - isSelected: boolean; - onClick: (category: string) => void; -}> = ({ category, isSelected, onClick }) => ( - <CategoryLabel - key={category} - className={isSelected ? 'selected' : ''} - onClick={() => onClick(category)} - > - {category} - </CategoryLabel> -); - /** Manages the viz type and the viz picker modal */ const VizTypeControl = (props: VizTypeControlProps) => { const { value: initialValue, onChange, isModalOpenInit, labelType } = props; const { mountedPluginMetadata } = usePluginContext(); const [showModal, setShowModal] = useState(!!isModalOpenInit); - const [searchInputValue, setSearchInputValue] = useState(''); - const [isSearching, setIsSearching] = useState(false); const [selectedViz, setSelectedViz] = useState(initialValue); const onSubmit = useCallback(() => { @@ -375,96 +86,16 @@ const VizTypeControl = (props: VizTypeControlProps) => { setShowModal(false); }, [selectedViz, onChange]); - const changeSearch: ChangeEventHandler<HTMLInputElement> = useCallback( - event => { - setSearchInputValue(event.target.value); - }, - [], - ); - - 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] = []; - } - result[category].push(entry); - }); - return result; - }, [chartMetadata]); - - // todo sort the categories - const categories = useMemo(() => Object.keys(chartsByCategory), [ - chartsByCategory, - ]); - - const [activeCategory, setActiveCategory] = useState<string>( - () => categories[0], - ); - - 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(() => { - setIsSearching(true); - }, []); - - const stopSearching = useCallback(() => { - setIsSearching(false); - setSearchInputValue(''); - }, []); - - const onTabClick = useCallback( - (key: string) => { - setActiveCategory(key); - stopSearching(); - }, - [stopSearching], - ); - const toggleModal = useCallback(() => { setShowModal(prevState => !prevState); // make sure the modal opens up to the last submitted viz setSelectedViz(initialValue); - setActiveCategory( - mountedPluginMetadata[initialValue]?.category || categories[0], - ); - }, [initialValue, mountedPluginMetadata, categories]); + }, [initialValue]); const labelContent = mountedPluginMetadata[initialValue]?.name || `${initialValue}`; - const selectedVizMetadata: ChartMetadata | undefined = - mountedPluginMetadata[selectedViz]; - - const vizEntriesToDisplay = isSearching - ? searchResults - : chartsByCategory[activeCategory] || []; - return ( <div> <ControlHeader {...props} /> @@ -494,76 +125,7 @@ const VizTypeControl = (props: VizTypeControlProps) => { maxWidth="1090px" responsive > - <VizPickerLayout> - <LeftPane> - <SearchWrapper> - <Input - type="text" - value={searchInputValue} - placeholder={t('Search')} - onChange={changeSearch} - onFocus={startSearching} - data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__search-input`} - prefix={ - <InputIconAlignment> - <Icons.Search iconSize="m" /> - </InputIconAlignment> - } - suffix={ - <InputIconAlignment> - {searchInputValue && ( - <Icons.XLarge iconSize="m" onClick={stopSearching} /> - )} - </InputIconAlignment> - } - /> - </SearchWrapper> - {categories.map(category => ( - <CategorySelector - key={category} - category={category} - isSelected={!isSearching && category === activeCategory} - onClick={onTabClick} - /> - ))} - </LeftPane> - - <ThumbnailGallery - vizEntries={vizEntriesToDisplay} - selectedViz={selectedViz} - setSelectedViz={setSelectedViz} - /> - - <DetailsPane> - <SectionTitle - css={css` - grid-area: viz-name; - `} - > - {selectedVizMetadata?.name} - </SectionTitle> - <Description> - {selectedVizMetadata?.description || - t('No description available.')} - </Description> - <SectionTitle - css={css` - grid-area: examples-header; - `} - > - {!!selectedVizMetadata?.exampleGallery?.length && t('Examples')} - </SectionTitle> - <Examples> - {(selectedVizMetadata?.exampleGallery || []).map(example => ( - <img - src={example.url} - alt={example.caption} - title={example.caption} - /> - ))} - </Examples> - </DetailsPane> - </VizPickerLayout> + <VizTypeGallery value={selectedViz} onChange={setSelectedViz} /> </UnpaddedModal> </div> );