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>
);
}