[Lens] Hide fix action when in embeddable (#213414)

## Summary

Related to #177932
Inline editing introduced a blurred boundary for the editing experience
in Lens, while the full editor relies on a Redux state manager to
dispatch state changes the inline editor has an hybrid approach.
Specifically the `fixAction` feature in the user messages system needs
access to the redux store which is not available within the embeddable
environment as for now.
So with this PR I've currently limited the scope provided for those
message with a `fixAction` and won't render the button any more.

For instance while investigating #177932 I've noticed that now the
embeddable doesn't crash any more, but rather shows the error message
with the `fixAction` button:

<img width="1496" alt="Screenshot 2025-03-06 at 16 07 35"
src="https://github.com/user-attachments/assets/1b2a5d73-56d9-4010-8a6f-82528efcb2ce"
/>

Note that clicking on the `Use filters` nothing will happen as
d2412a5f98/x-pack/platform/plugins/shared/lens/public/react_embeddable/user_messages/api.ts (L195)
is a mock. A simple `updateAttributes` call here won't suffice as it
requires the logic behind `updateDatasourceState` slice to harmonize the
datasource changes with the visualisation counter part.
With this PR the message will hide the button and render as follow:

<img width="1498" alt="Screenshot 2025-03-06 at 16 18 55"
src="https://github.com/user-attachments/assets/01d55f6c-7563-4e07-a18e-35d1062a8d79"
/>

It is a temporary fix but at least it won't feel broken.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
Marco Liberati 2025-03-19 09:55:34 +01:00 committed by GitHub
parent b731d759e8
commit 3c3038b855
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 95 additions and 62 deletions

View file

@ -934,7 +934,7 @@ function blankLayer(indexPatternId: string, linkToLayers?: string[]): FormBasedL
function getLayerErrorMessages(
state: FormBasedPrivateState,
framePublicAPI: FramePublicAPI,
setState: StateSetter<FormBasedPrivateState, unknown>,
setState: StateSetter<FormBasedPrivateState, unknown> | undefined,
core: CoreStart,
data: DataPublicPluginStart
): UserMessage[] {
@ -962,7 +962,7 @@ function getLayerErrorMessages(
) : (
<>
{error.message}
{error.fixAction && (
{error.fixAction && setState && (
<EuiButton
data-test-subj="errorFixAction"
onClick={async () => {

View file

@ -150,12 +150,12 @@ export function getDisallowedTermsMessage(
currentColumn.sourceField,
...(currentColumn.params?.secondaryFields ?? []),
];
const table = frame.activeData?.[layerId] || frame.activeData?.default;
const activeDataFieldNameMatch =
frame.activeData?.[layerId].columns.find(({ id }) => id === columnId)?.meta.field ===
fieldNames[0];
table?.columns.find(({ id }) => id === columnId)?.meta.field === fieldNames[0];
let currentTerms = uniq(
frame.activeData?.[layerId].rows
table?.rows
.map((row) => row[columnId] as string | MultiFieldKeyFormat)
.filter((term) =>
fieldNames.length > 1

View file

@ -10,6 +10,7 @@ import { CoreStart } from '@kbn/core/public';
import type { Query } from '@kbn/es-query';
import memoizeOne from 'memoize-one';
import { DataPublicPluginStart, UI_SETTINGS } from '@kbn/data-plugin/public';
import { nonNullable } from '../../../utils';
import type { DateRange } from '../../../../common/types';
import type {
DatasourceFixAction,
@ -1567,7 +1568,7 @@ export function getErrorMessages(
const skippedColumns = visibleManagedReferences.flatMap(([columnId]) =>
getManagedColumnsFrom(columnId, layer.columns).map(([id]) => id)
);
const errors = columns
const errors: LayerErrorMessage[] = columns
.flatMap(([columnId, column]) => {
if (skippedColumns.includes(columnId)) {
return;
@ -1606,7 +1607,7 @@ export function getErrorMessages(
};
})
// remove the undefined values
.filter((v) => v != null) as LayerErrorMessage[];
.filter(nonNullable) as LayerErrorMessage[];
return errors.length ? errors : undefined;
}

View file

@ -134,6 +134,22 @@ describe('indexpattern_datasource utils', () => {
expect(setStateMock).toHaveBeenCalledTimes(1);
});
test('should not render a fix link if no setState is provided', () => {
framePublicAPI.activeData!.id.columns[0].meta.sourceParams!.hasPrecisionError = true;
const warningMessages = getPrecisionErrorWarningMessages(
datatableUtilitites,
state,
framePublicAPI,
docLinks
);
render(<I18nProvider>{getLongMessage(warningMessages[0])}</I18nProvider>);
screen.debug();
// Make sure the message is there before checking the absence of the link/button
expect(screen.getByText(/might be an approximation./)).toBeInTheDocument();
expect(screen.queryByText('Enable accuracy mode')).toBe(null);
});
test('should other suggestions if accuracy mode already enabled', async () => {
framePublicAPI.activeData!.id.columns[0].meta.sourceParams!.hasPrecisionError = true;
(state.layers.id.columns.col1 as TermsIndexPatternColumn).params.accuracyMode = true;

View file

@ -182,7 +182,7 @@ export function fieldIsInvalid(
const accuracyModeDisabledWarning = (
columnName: string,
columnId: string,
enableAccuracyMode: () => void
enableAccuracyMode?: () => void
): UserMessage => ({
uniqueId: PRECISION_ERROR_ACCURACY_MODE_DISABLED,
severity: 'warning',
@ -204,12 +204,16 @@ const accuracyModeDisabledWarning = (
name: <strong>{columnName}</strong>,
}}
/>
<EuiSpacer size="s" />
<EuiLink data-test-subj="lnsPrecisionWarningEnableAccuracy" onClick={enableAccuracyMode}>
{i18n.translate('xpack.lens.indexPattern.enableAccuracyMode', {
defaultMessage: 'Enable accuracy mode',
})}
</EuiLink>
{enableAccuracyMode ? (
<>
<EuiSpacer size="s" />
<EuiLink data-test-subj="lnsPrecisionWarningEnableAccuracy" onClick={enableAccuracyMode}>
{i18n.translate('xpack.lens.indexPattern.enableAccuracyMode', {
defaultMessage: 'Enable accuracy mode',
})}
</EuiLink>
</>
) : null}
</>
),
});
@ -442,7 +446,7 @@ export function getPrecisionErrorWarningMessages(
state: FormBasedPrivateState,
{ activeData, dataViews }: FramePublicAPI,
docLinks: DocLinksStart,
setState: StateSetter<FormBasedPrivateState>
setState?: StateSetter<FormBasedPrivateState>
) {
const warningMessages: UserMessage[] = [];
@ -491,23 +495,29 @@ export function getPrecisionErrorWarningMessages(
column.id,
docLinks.links.aggs.terms_doc_count_error
)
: accuracyModeDisabledWarning(column.name, column.id, () => {
setState((prevState) =>
mergeLayer({
state: prevState,
layerId,
newLayer: updateDefaultLabels(
updateColumnParam({
layer: currentLayer,
columnId: column.id,
paramName: 'accuracyMode',
value: true,
}),
indexPattern
),
})
);
})
: accuracyModeDisabledWarning(
column.name,
column.id,
setState
? () => {
setState((prevState) =>
mergeLayer({
state: prevState,
layerId,
newLayer: updateDefaultLabels(
updateColumnParam({
layer: currentLayer,
columnId: column.id,
paramName: 'accuracyMode',
value: true,
}),
indexPattern
),
})
);
}
: undefined
)
);
} else {
warningMessages.push({
@ -545,33 +555,37 @@ export function getPrecisionErrorWarningMessages(
),
}}
/>
<EuiSpacer size="s" />
<EuiLink
onClick={() => {
setState((prevState) =>
mergeLayer({
state: prevState,
layerId,
newLayer: updateDefaultLabels(
updateColumnParam({
layer: currentLayer,
columnId: column.id,
paramName: 'orderBy',
value: {
type: 'rare',
maxDocCount: DEFAULT_MAX_DOC_COUNT,
},
}),
indexPattern
),
})
);
}}
>
{i18n.translate('xpack.lens.indexPattern.switchToRare', {
defaultMessage: 'Rank by rarity',
})}
</EuiLink>
{setState ? (
<>
<EuiSpacer size="s" />
<EuiLink
onClick={() => {
setState((prevState) =>
mergeLayer({
state: prevState,
layerId,
newLayer: updateDefaultLabels(
updateColumnParam({
layer: currentLayer,
columnId: column.id,
paramName: 'orderBy',
value: {
type: 'rare',
maxDocCount: DEFAULT_MAX_DOC_COUNT,
},
}),
indexPattern
),
})
);
}}
>
{i18n.translate('xpack.lens.indexPattern.switchToRare', {
defaultMessage: 'Rank by rarity',
})}
</EuiLink>
</>
) : null}
</>
),
fixableInEditor: true,

View file

@ -192,7 +192,8 @@ export function buildUserMessagesHelpers(
userMessages.push(
...(activeDatasource?.getUserMessages(activeDatasourceState, {
setState: () => {},
// limit the fixAction within the embeddable for now
setState: undefined,
frame: framePublicAPI,
visualizationInfo: activeVisualization?.getVisualizationInfo?.(
activeVisualizationState,

View file

@ -480,7 +480,7 @@ export interface Datasource<T = unknown, P = unknown, Q = Query | AggregateQuery
state: T,
deps: {
frame: FramePublicAPI;
setState: StateSetter<T>;
setState?: StateSetter<T>;
visualizationInfo?: VisualizationInfo;
}
) => UserMessage[];
@ -532,6 +532,7 @@ export interface Datasource<T = unknown, P = unknown, Q = Query | AggregateQuery
export interface DatasourceFixAction<T> {
label: string;
isCompatible?: (frame: FramePublicAPI) => boolean;
newState: (frame: FramePublicAPI) => Promise<T>;
}

View file

@ -260,7 +260,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await lens.assertFocusedField('clientip');
});
it('should duplicate an element in a group', async () => {
await lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 0, 1);
await lens.dimensionKeyboardDragDrop('lnsXY_yDimensionPanel', 0, 2);
expect(await lens.getDimensionTriggersTexts('lnsXY_yDimensionPanel')).to.eql([
'Count of records',
'Median of bytes',