export default function VizTypeGallery()

in superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx [438:840]


export default function VizTypeGallery(props: VizTypeGalleryProps) {
  const { selectedViz, onChange, onDoubleClick, className, denyList } = props;
  const { mountedPluginMetadata } = usePluginContext();
  const searchInputRef = useRef<HTMLInputElement>();
  const [searchInputValue, setSearchInputValue] = useState('');
  const [isSearchFocused, setIsSearchFocused] = useState(true);
  const isActivelySearching = isSearchFocused && !!searchInputValue;

  const selectedVizMetadata: ChartMetadata | null = selectedViz
    ? mountedPluginMetadata[selectedViz]
    : null;

  const chartMetadata: VizEntry[] = useMemo(() => {
    const result = Object.entries(mountedPluginMetadata)
      .map(([key, value]) => ({ key, value }))
      .filter(({ key }) => !denyList.includes(key))
      .filter(
        ({ value }) =>
          nativeFilterGate(value.behaviors || []) && !value.deprecated,
      )
      .sort((a, b) => a.value.name.localeCompare(b.value.name));
    return result;
  }, [mountedPluginMetadata, denyList]);

  const chartsByCategory = useMemo(() => {
    const result: Record<string, VizEntry[]> = {};
    chartMetadata.forEach(entry => {
      const category = entry.value.category || OTHER_CATEGORY;
      if (!result[category]) {
        result[category] = [];
      }
      result[category].push(entry);
    });
    return result;
  }, [chartMetadata]);

  const categories = useMemo(
    () =>
      Object.keys(chartsByCategory).sort((a, b) => {
        // make sure Other goes at the end
        if (a === OTHER_CATEGORY) return 1;
        if (b === OTHER_CATEGORY) return -1;
        // sort alphabetically
        return a.localeCompare(b);
      }),
    [chartsByCategory],
  );

  const chartsByTags = useMemo(() => {
    const result: Record<string, VizEntry[]> = {};
    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),
    [chartsByTags],
  );

  const sortedMetadata = useMemo(
    () =>
      chartMetadata.sort((a, b) => a.value.name.localeCompare(b.value.name)),
    [chartMetadata],
  );

  const [activeSelector, setActiveSelector] = useState<string>(
    () => selectedVizMetadata?.category || FEATURED,
  );

  const [activeSection, setActiveSection] = useState<string>(() =>
    selectedVizMetadata?.category ? Sections.Category : Sections.Featured,
  );

  // get a fuse instance for fuzzy search
  const fuse = useMemo(
    () =>
      new Fuse(chartMetadata, {
        ignoreLocation: true,
        threshold: 0.3,
        keys: [
          {
            name: 'value.name',
            weight: 4,
          },
          {
            name: 'value.tags',
            weight: 2,
          },
          'value.description',
        ],
      }),
    [chartMetadata],
  );

  const searchResults = useMemo(() => {
    if (searchInputValue.trim() === '') {
      return [];
    }
    return fuse
      .search(searchInputValue)
      .map(result => result.item)
      .sort((a, b) => {
        const aLabel = a.value?.label;
        const bLabel = b.value?.label;
        const aOrder =
          aLabel && chartLabelWeight[aLabel]
            ? chartLabelWeight[aLabel].weight
            : 0;
        const bOrder =
          bLabel && chartLabelWeight[bLabel]
            ? chartLabelWeight[bLabel].weight
            : 0;
        return bOrder - aOrder;
      });
  }, [searchInputValue, fuse]);

  const focusSearch = useCallback(() => {
    // "start searching" is actually a two-stage process.
    // When you first click on the search bar, the input is focused and nothing else happens.
    // Once you begin typing, the selected category is cleared and the displayed viz entries change.
    setIsSearchFocused(true);
  }, []);

  const changeSearch: ChangeEventHandler<HTMLInputElement> = useCallback(
    event => setSearchInputValue(event.target.value),
    [],
  );

  const stopSearching = useCallback(() => {
    // stopping a search takes you back to the category you were looking at before.
    // Unlike focusSearch, this is a simple one-step process.
    setIsSearchFocused(false);
    setSearchInputValue('');
    searchInputRef.current!.blur();
  }, []);

  const clickSelector = useCallback(
    (selector: string, sectionId: string) => {
      if (isSearchFocused) {
        stopSearching();
      }
      setActiveSelector(selector);
      setActiveSection(sectionId);
      // clear the selected viz if it is not present in the new category or tags
      const isSelectedVizCompatible =
        selectedVizMetadata &&
        doesVizMatchSelector(selectedVizMetadata, selector);
      if (selector !== activeSelector && !isSelectedVizCompatible) {
        onChange(null);
      }
    },
    [
      stopSearching,
      isSearchFocused,
      activeSelector,
      selectedVizMetadata,
      onChange,
    ],
  );

  const sectionMap = useMemo(
    () => ({
      [Sections.Category]: {
        title: t('Category'),
        icon: <Icons.Category iconSize="m" />,
        selectors: categories,
      },
      [Sections.Tags]: {
        title: t('Tags'),
        icon: <Icons.NumberOutlined iconSize="m" />,
        selectors: tags,
      },
    }),
    [categories, tags],
  );

  const getVizEntriesToDisplay = () => {
    if (isActivelySearching) {
      return searchResults;
    }
    if (activeSelector === ALL_CHARTS && activeSection === Sections.AllCharts) {
      return sortedMetadata;
    }
    if (
      activeSelector === FEATURED &&
      activeSection === Sections.Featured &&
      chartsByTags[FEATURED]
    ) {
      return chartsByTags[FEATURED];
    }
    if (
      activeSection === Sections.Category &&
      chartsByCategory[activeSelector]
    ) {
      return chartsByCategory[activeSelector];
    }
    if (activeSection === Sections.Tags && chartsByTags[activeSelector]) {
      return chartsByTags[activeSelector];
    }
    return [];
  };

  return (
    <VizPickerLayout
      className={className}
      isSelectedVizMetadata={Boolean(selectedVizMetadata)}
    >
      <LeftPane>
        <Selector
          css={({ gridUnit }) =>
            // adjust style for not being inside a collapse
            css`
              margin: ${gridUnit * 2}px;
              margin-bottom: 0;
            `
          }
          sectionId={Sections.AllCharts}
          selector={ALL_CHARTS}
          icon={<Icons.Ballot iconSize="m" />}
          isSelected={
            !isActivelySearching &&
            ALL_CHARTS === activeSelector &&
            Sections.AllCharts === activeSection
          }
          onClick={clickSelector}
        />
        <Selector
          css={({ gridUnit }) =>
            // adjust style for not being inside a collapse
            css`
              margin: ${gridUnit * 2}px;
              margin-bottom: 0;
            `
          }
          sectionId={Sections.Featured}
          selector={FEATURED}
          icon={<Icons.FireOutlined iconSize="m" />}
          isSelected={
            !isActivelySearching &&
            FEATURED === activeSelector &&
            Sections.Featured === activeSection
          }
          onClick={clickSelector}
        />
        <AntdCollapse
          expandIconPosition="right"
          ghost
          defaultActiveKey={Sections.Category}
        >
          {Object.keys(sectionMap).map(sectionId => {
            const section = sectionMap[sectionId as keyof typeof sectionMap];

            return (
              <AntdCollapse.Panel
                header={<span className="header">{section.title}</span>}
                key={sectionId}
              >
                {section.selectors.map((selector: string) => (
                  <Selector
                    key={selector}
                    selector={selector}
                    sectionId={sectionId}
                    icon={section.icon}
                    isSelected={
                      !isActivelySearching &&
                      selector === activeSelector &&
                      sectionId === activeSection
                    }
                    onClick={clickSelector}
                  />
                ))}
              </AntdCollapse.Panel>
            );
          })}
        </AntdCollapse>
      </LeftPane>

      <SearchWrapper>
        <Input
          type="text"
          ref={searchInputRef as any /* cast required because emotion */}
          value={searchInputValue}
          placeholder={t('Search all charts')}
          onChange={changeSearch}
          onFocus={focusSearch}
          data-test={`${VIZ_TYPE_CONTROL_TEST_ID}__search-input`}
          prefix={
            <InputIconAlignment>
              <Icons.SearchOutlined iconSize="m" />
            </InputIconAlignment>
          }
          suffix={
            <InputIconAlignment>
              {searchInputValue && (
                <Icons.CloseOutlined iconSize="m" onClick={stopSearching} />
              )}
            </InputIconAlignment>
          }
        />
      </SearchWrapper>

      <RightPane>
        <ThumbnailGallery
          vizEntries={getVizEntriesToDisplay()}
          selectedViz={selectedViz}
          setSelectedViz={onChange}
          onDoubleClick={onDoubleClick}
        />
      </RightPane>

      {selectedVizMetadata ? (
        <div
          css={(theme: SupersetTheme) => [
            DetailsPane(theme),
            DetailsPopulated(theme),
          ]}
        >
          <>
            <SectionTitle
              css={css`
                grid-area: viz-name;
                position: relative;
              `}
            >
              {selectedVizMetadata?.name}
              {selectedVizMetadata?.label && (
                <Tooltip
                  id="viz-badge-tooltip"
                  placement="top"
                  title={
                    selectedVizMetadata.labelExplanation ??
                    chartLabelExplanations[selectedVizMetadata.label]
                  }
                >
                  <TitleLabelWrapper>
                    <HighlightLabel>
                      <div>{t(selectedVizMetadata.label)}</div>
                    </HighlightLabel>
                  </TitleLabelWrapper>
                </Tooltip>
              )}
            </SectionTitle>
            <TagsWrapper>
              {selectedVizMetadata?.tags.map(tag => (
                <Label
                  key={tag}
                  css={({ gridUnit }) => css`
                    margin-bottom: ${gridUnit * 2}px;
                  `}
                >
                  {tag}
                </Label>
              ))}
            </TagsWrapper>
            <Description>
              {selectedVizMetadata?.description ||
                t('No description available.')}
            </Description>
            <SectionTitle
              css={css`
                grid-area: examples-header;
              `}
            >
              {t('Examples')}
            </SectionTitle>
            <Examples>
              {(selectedVizMetadata?.exampleGallery?.length
                ? selectedVizMetadata.exampleGallery
                : [
                    {
                      url: selectedVizMetadata?.thumbnail,
                      caption: selectedVizMetadata?.name,
                    },
                  ]
              ).map(example => (
                <img
                  key={example.url}
                  src={example.url}
                  alt={example.caption}
                  title={example.caption}
                />
              ))}
            </Examples>
          </>
        </div>
      ) : null}
    </VizPickerLayout>
  );
}