export function SuggestionPanel()

in x-pack/platform/plugins/shared/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx [310:686]


export function SuggestionPanel({
  datasourceMap,
  visualizationMap,
  frame,
  ExpressionRenderer: ExpressionRendererComponent,
  getUserMessages,
  nowProvider,
  core,
  showOnlyIcons,
  wrapSuggestions,
  toggleAccordionCb,
  isAccordionOpen,
}: SuggestionPanelProps) {
  const dispatchLens = useLensDispatch();
  const activeDatasourceId = useLensSelector(selectActiveDatasourceId);
  const activeData = useLensSelector(selectStagedActiveData);
  const datasourceStates = useLensSelector(selectDatasourceStates);
  const existsStagedPreview = useLensSelector((state) => Boolean(state.lens.stagedPreview));
  const currentVisualization = useLensSelector(selectCurrentVisualization);
  const currentDatasourceStates = useLensSelector(selectCurrentDatasourceStates);
  const euiThemeContext = useEuiTheme();
  const { euiTheme } = euiThemeContext;

  const framePublicAPI = useLensSelector((state) => selectFramePublicAPI(state, datasourceMap));
  const changesApplied = useLensSelector(selectChangesApplied);
  // get user's selection from localStorage, this key defines if the suggestions panel will be hidden or not
  const initialAccordionStatusValue = isAccordionOpen != null ? !Boolean(isAccordionOpen) : false;
  const [hideSuggestions, setHideSuggestions] = useLocalStorage(
    LOCAL_STORAGE_SUGGESTIONS_PANEL,
    initialAccordionStatusValue
  );
  useEffect(() => {
    if (isAccordionOpen != null) {
      setHideSuggestions(!Boolean(isAccordionOpen));
    }
  }, [isAccordionOpen, setHideSuggestions]);

  const toggleSuggestions = useCallback(() => {
    setHideSuggestions(!hideSuggestions);
    toggleAccordionCb?.(!hideSuggestions);
  }, [setHideSuggestions, hideSuggestions, toggleAccordionCb]);

  const missingIndexPatterns = getMissingIndexPattern(
    activeDatasourceId ? datasourceMap[activeDatasourceId] : null,
    activeDatasourceId ? datasourceStates[activeDatasourceId] : null,
    frame.dataViews.indexPatterns
  );
  const { suggestions, currentStateExpression, currentStateError } = useMemo(() => {
    const newSuggestions = missingIndexPatterns.length
      ? []
      : getSuggestions({
          datasourceMap,
          datasourceStates: currentDatasourceStates,
          visualizationMap,
          activeVisualization: currentVisualization.activeId
            ? visualizationMap[currentVisualization.activeId]
            : undefined,
          visualizationState: currentVisualization.state,
          activeData,
          dataViews: frame.dataViews,
        })
          .filter(
            ({
              hide,
              visualizationId,
              visualizationState: suggestionVisualizationState,
              datasourceState: suggestionDatasourceState,
              datasourceId: suggestionDatasourceId,
            }) => {
              return (
                !hide &&
                configurationsValid(
                  suggestionDatasourceId ? datasourceMap[suggestionDatasourceId] : null,
                  suggestionDatasourceState,
                  visualizationMap[visualizationId],
                  suggestionVisualizationState,
                  framePublicAPI
                )
              );
            }
          )
          .slice(0, MAX_SUGGESTIONS_DISPLAYED)
          .map((suggestion) => ({
            ...suggestion,
            previewExpression: preparePreviewExpression(
              suggestion,
              visualizationMap[suggestion.visualizationId],
              datasourceMap,
              currentDatasourceStates,
              frame,
              nowProvider
            ),
          }));

    const hasErrors = getUserMessages
      ? getUserMessages(['visualization', 'visualizationInEditor'], { severity: 'error' }).length >
        0
      : false;

    const newStateExpression =
      currentVisualization.state && currentVisualization.activeId && !hasErrors
        ? preparePreviewExpression(
            { visualizationState: currentVisualization.state },
            visualizationMap[currentVisualization.activeId],
            datasourceMap,
            currentDatasourceStates,
            frame,
            nowProvider
          )
        : undefined;

    return {
      suggestions: newSuggestions,
      currentStateExpression: newStateExpression,
      currentStateError: hasErrors,
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    currentDatasourceStates,
    currentVisualization.state,
    currentVisualization.activeId,
    activeDatasourceId,
    datasourceMap,
    visualizationMap,
    activeData,
  ]);

  const context: ExecutionContextSearch = useLensSelector(selectExecutionContextSearch);
  const searchSessionId = useLensSelector(selectSearchSessionId);

  const contextRef = useRef<ExecutionContextSearch>(context);
  contextRef.current = context;

  const sessionIdRef = useRef<string>(searchSessionId);
  sessionIdRef.current = searchSessionId;

  const AutoRefreshExpressionRenderer = useMemo(() => {
    return (props: ReactExpressionRendererProps) => (
      <ExpressionRendererComponent
        {...props}
        searchContext={contextRef.current}
        searchSessionId={sessionIdRef.current}
      />
    );
  }, [ExpressionRendererComponent]);

  const [lastSelectedSuggestion, setLastSelectedSuggestion] = useState<number>(-1);

  useEffect(() => {
    // if the staged preview is overwritten by a suggestion,
    // reset the selected index to "current visualization" because
    // we are not in transient suggestion state anymore
    if (!existsStagedPreview && lastSelectedSuggestion !== -1) {
      setLastSelectedSuggestion(-1);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [existsStagedPreview]);

  const startTime = useRef<number>(0);
  const initialRenderComplete = useRef<boolean>(false);
  const suggestionsRendered = useRef<boolean[]>([]);
  const totalSuggestions = suggestions.length + 1;

  const onSuggestionRender = useCallback(
    (suggestionIndex: number) => {
      suggestionsRendered.current[suggestionIndex] = true;
      if (initialRenderComplete.current === false && suggestionsRendered.current.every(Boolean)) {
        initialRenderComplete.current = true;
        reportPerformanceMetricEvent(core.analytics, {
          eventName: 'lensSuggestionsRenderTime',
          duration: performance.now() - startTime.current,
        });
      }
    },
    [core.analytics]
  );

  const rollbackToCurrentVisualization = useCallback(() => {
    if (lastSelectedSuggestion !== -1) {
      setLastSelectedSuggestion(-1);
      dispatchLens(rollbackSuggestion());
      dispatchLens(applyChanges());
    }
  }, [dispatchLens, lastSelectedSuggestion]);

  if (!activeDatasourceId) {
    return null;
  }

  if (suggestions.length === 0) {
    return null;
  }

  const renderApplyChangesPrompt = () => (
    <EuiPanel
      hasShadow={false}
      className="lnsSuggestionPanel__applyChangesPrompt"
      paddingSize="m"
      css={css`
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        height: 100px;
        background-color: ${euiTheme.colors.lightestShade} !important;
      `}
    >
      <EuiText size="s" color="subdued" className="lnsSuggestionPanel__applyChangesMessage">
        <p>
          <FormattedMessage
            id="xpack.lens.suggestions.applyChangesPrompt"
            defaultMessage="Latest changes must be applied to view suggestions."
          />
        </p>
      </EuiText>

      <EuiButtonEmpty
        iconType="checkInCircleFilled"
        size="s"
        className={DONT_CLOSE_DIMENSION_CONTAINER_ON_CLICK_CLASS}
        onClick={() => dispatchLens(applyChanges())}
        data-test-subj="lnsApplyChanges__suggestions"
      >
        <FormattedMessage
          id="xpack.lens.suggestions.applyChangesLabel"
          defaultMessage="Apply changes"
        />
      </EuiButtonEmpty>
    </EuiPanel>
  );

  const renderSuggestionsUI = () => {
    suggestionsRendered.current = new Array(totalSuggestions).fill(false);
    startTime.current = performance.now();
    return (
      <>
        {currentVisualization.activeId && !hideSuggestions && (
          <SuggestionPreview
            preview={{
              error: currentStateError,
              expression: !showOnlyIcons ? currentStateExpression : undefined,
              icon:
                visualizationMap[currentVisualization.activeId].getDescription(
                  currentVisualization.state
                ).icon || 'empty',
              title: i18n.translate('xpack.lens.suggestions.currentVisLabel', {
                defaultMessage: 'Current visualization',
              }),
            }}
            ExpressionRenderer={AutoRefreshExpressionRenderer}
            onSelect={rollbackToCurrentVisualization}
            selected={lastSelectedSuggestion === -1}
            showTitleAsLabel
            onRender={() => onSuggestionRender(0)}
            wrapSuggestions={wrapSuggestions}
          />
        )}
        {!hideSuggestions &&
          suggestions.map((suggestion, index) => {
            return (
              <SuggestionPreview
                preview={{
                  expression: !showOnlyIcons ? suggestion.previewExpression : undefined,
                  icon: suggestion.previewIcon,
                  title: suggestion.title,
                }}
                ExpressionRenderer={AutoRefreshExpressionRenderer}
                key={index}
                onSelect={() => {
                  if (lastSelectedSuggestion === index) {
                    rollbackToCurrentVisualization();
                  } else {
                    setLastSelectedSuggestion(index);
                    switchToSuggestion(dispatchLens, suggestion, { applyImmediately: true });
                  }
                }}
                selected={index === lastSelectedSuggestion}
                onRender={() => onSuggestionRender(index + 1)}
                showTitleAsLabel={showOnlyIcons}
                wrapSuggestions={wrapSuggestions}
              />
            );
          })}
      </>
    );
  };
  const title = (
    <EuiTitle
      size="xxs"
      css={css`
        padding: 2px;
      `}
    >
      <h3>
        <FormattedMessage
          id="xpack.lens.editorFrame.suggestionPanelTitle"
          defaultMessage="Suggestions"
        />
      </h3>
    </EuiTitle>
  );
  return (
    <EuiAccordion
      id="lensSuggestionsPanel"
      buttonProps={{
        'data-test-subj': 'lensSuggestionsPanelToggleButton',
        paddingSize: wrapSuggestions ? 'm' : 's',
      }}
      css={css`
        padding-bottom: ${wrapSuggestions ? 0 : euiThemeVars.euiSizeS};
        .euiAccordion__buttonContent {
          width: 100%;
        }
      `}
      buttonContent={title}
      forceState={hideSuggestions ? 'closed' : 'open'}
      onToggle={toggleSuggestions}
      extraAction={
        <>
          {!hideSuggestions && (
            <>
              {existsStagedPreview && (
                <EuiToolTip
                  content={i18n.translate('xpack.lens.suggestion.refreshSuggestionTooltip', {
                    defaultMessage: 'Refresh the suggestions based on the selected visualization.',
                  })}
                >
                  <EuiButtonEmpty
                    data-test-subj="lensSubmitSuggestion"
                    size="xs"
                    iconType="refresh"
                    onClick={() => {
                      dispatchLens(submitSuggestion());
                    }}
                  >
                    {i18n.translate('xpack.lens.sugegstion.refreshSuggestionLabel', {
                      defaultMessage: 'Refresh',
                    })}
                  </EuiButtonEmpty>
                </EuiToolTip>
              )}
            </>
          )}
          {wrapSuggestions && (
            <EuiNotificationBadge size="m" color="subdued">
              {suggestions.length + 1}
            </EuiNotificationBadge>
          )}
        </>
      }
    >
      <div
        className="eui-scrollBar"
        data-test-subj="lnsSuggestionsPanel"
        role="list"
        tabIndex={0}
        css={css`
          flex-wrap: ${wrapSuggestions ? 'wrap' : 'nowrap'};
          gap: ${wrapSuggestions ? euiTheme.size.base : 0};
          overflow-x: scroll;
          overflow-y: hidden;
          display: flex;
          padding-top: ${euiTheme.size.xs};
          mask-image: linear-gradient(
            to right,
            ${transparentize(euiTheme.colors.danger, 0.1)} 0%,
            ${euiTheme.colors.danger} 5px,
            ${euiTheme.colors.danger} calc(100% - 5px),
            ${transparentize(euiTheme.colors.danger, 0.1)} 100%
          );
        `}
      >
        {changesApplied ? renderSuggestionsUI() : renderApplyChangesPrompt()}
      </div>
    </EuiAccordion>
  );
}