Skip to content

Commit 35ff37a

Browse files
author
Wylie Conlon
authored
[Lens] Fix switching with layers (#71982)
* [Lens] Fix chart switching with multiple layers * Unskip Lens smokescreen test * Fix types * Revert <p> change
1 parent 679209b commit 35ff37a

File tree

9 files changed

+214
-18
lines changed

9 files changed

+214
-18
lines changed

x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ function LayerPanels(
129129
className="lnsConfigPanel__addLayerBtn"
130130
fullWidth
131131
size="s"
132-
data-test-subj="lnsXY_layer_add"
132+
data-test-subj="lnsLayerAddButton"
133133
aria-label={i18n.translate('xpack.lens.xyChart.addLayerButton', {
134134
defaultMessage: 'Add layer',
135135
})}

x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,14 @@ describe('LayerPanel', () => {
9393
describe('layer reset and remove', () => {
9494
it('should show the reset button when single layer', () => {
9595
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} />);
96-
expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain(
96+
expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
9797
'Reset layer'
9898
);
9999
});
100100

101101
it('should show the delete button when multiple layers', () => {
102102
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} isOnlyLayer={false} />);
103-
expect(component.find('[data-test-subj="lns_layer_remove"]').first().text()).toContain(
103+
expect(component.find('[data-test-subj="lnsLayerRemove"]').first().text()).toContain(
104104
'Delete layer'
105105
);
106106
});
@@ -109,7 +109,7 @@ describe('LayerPanel', () => {
109109
const cb = jest.fn();
110110
const component = mountWithIntl(<LayerPanel {...getDefaultProps()} onRemoveLayer={cb} />);
111111
act(() => {
112-
component.find('[data-test-subj="lns_layer_remove"]').first().simulate('click');
112+
component.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
113113
});
114114
expect(cb).toHaveBeenCalled();
115115
});

x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ export function LayerPanel(
429429
size="xs"
430430
iconType="trash"
431431
color="danger"
432-
data-test-subj="lns_layer_remove"
432+
data-test-subj="lnsLayerRemove"
433433
onClick={() => {
434434
// If we don't blur the remove / clear button, it remains focused
435435
// which is a strange UX in this case. e.target.blur doesn't work

x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.test.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ describe('chart_switch', () => {
4646
};
4747
}
4848

49+
/**
50+
* There are three visualizations. Each one has the same suggestion behavior:
51+
*
52+
* visA: suggests an empty state
53+
* visB: suggests an empty state
54+
* visC:
55+
* - Never switches to subvisC2
56+
* - Allows a switch to subvisC3
57+
* - Allows a switch to subvisC1
58+
*/
4959
function mockVisualizations() {
5060
return {
5161
visA: generateVisualization('visA'),
@@ -292,6 +302,49 @@ describe('chart_switch', () => {
292302
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert');
293303
});
294304

305+
it('should support multi-layer suggestions without data loss', () => {
306+
const dispatch = jest.fn();
307+
const visualizations = mockVisualizations();
308+
const frame = mockFrame(['a', 'b']);
309+
310+
const datasourceMap = mockDatasourceMap();
311+
datasourceMap.testDatasource.getDatasourceSuggestionsFromCurrentState.mockReturnValue([
312+
{
313+
state: {},
314+
table: {
315+
columns: [
316+
{
317+
columnId: 'a',
318+
operation: {
319+
label: '',
320+
dataType: 'string',
321+
isBucketed: true,
322+
},
323+
},
324+
],
325+
isMultiRow: true,
326+
layerId: 'a',
327+
changeType: 'unchanged',
328+
},
329+
keptLayerIds: ['a', 'b'],
330+
},
331+
]);
332+
333+
const component = mount(
334+
<ChartSwitch
335+
visualizationId="visA"
336+
visualizationState={{}}
337+
visualizationMap={visualizations}
338+
dispatch={dispatch}
339+
framePublicAPI={frame}
340+
datasourceMap={datasourceMap}
341+
datasourceStates={mockDatasourceStates()}
342+
/>
343+
);
344+
345+
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined();
346+
});
347+
295348
it('should indicate data loss if no data will be used', () => {
296349
const dispatch = jest.fn();
297350
const visualizations = mockVisualizations();

x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export function ChartSwitch(props: Props) {
139139
dataLoss = 'nothing';
140140
} else if (!topSuggestion) {
141141
dataLoss = 'everything';
142-
} else if (layers.length > 1) {
142+
} else if (layers.length > 1 && layers.length !== topSuggestion.keptLayerIds.length) {
143143
dataLoss = 'layers';
144144
} else if (topSuggestion.columns !== layers[0][1].getTableSpec().length) {
145145
dataLoss = 'columns';
@@ -258,14 +258,15 @@ function getTopSuggestion(
258258
newVisualization: Visualization<unknown, unknown>,
259259
subVisualizationId?: string
260260
): Suggestion | undefined {
261-
const suggestions = getSuggestions({
261+
const unfilteredSuggestions = getSuggestions({
262262
datasourceMap: props.datasourceMap,
263263
datasourceStates: props.datasourceStates,
264264
visualizationMap: { [visualizationId]: newVisualization },
265265
activeVisualizationId: props.visualizationId,
266266
visualizationState: props.visualizationState,
267267
subVisualizationId,
268-
}).filter((suggestion) => {
268+
});
269+
const suggestions = unfilteredSuggestions.filter((suggestion) => {
269270
// don't use extended versions of current data table on switching between visualizations
270271
// to avoid confusing the user.
271272
return (

x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '../types';
1414
import { State, XYState, visualizationTypes } from './types';
1515
import { generateId } from '../id_generator';
16+
import { xyVisualization } from './xy_visualization';
1617

1718
jest.mock('../id_generator');
1819

@@ -119,7 +120,33 @@ describe('xy_suggestions', () => {
119120
});
120121

121122
expect(suggestions).toHaveLength(visualizationTypes.length);
122-
expect(suggestions.map(({ state }) => state.preferredSeriesType)).toEqual([
123+
expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([
124+
'bar_stacked',
125+
'area_stacked',
126+
'area',
127+
'line',
128+
'bar_horizontal_stacked',
129+
'bar_horizontal',
130+
'bar',
131+
]);
132+
});
133+
134+
// This limitation is acceptable for now, but is now tested
135+
test('is unable to generate layers when switching from a non-XY chart with multiple layers', () => {
136+
(generateId as jest.Mock).mockReturnValueOnce('aaa');
137+
const suggestions = getSuggestions({
138+
table: {
139+
isMultiRow: true,
140+
columns: [numCol('bytes'), dateCol('date')],
141+
layerId: 'first',
142+
changeType: 'unchanged',
143+
},
144+
keptLayerIds: ['first', 'second'],
145+
});
146+
147+
expect(suggestions).toHaveLength(visualizationTypes.length);
148+
expect(suggestions.map(({ state }) => state.layers.length)).toEqual([1, 1, 1, 1, 1, 1, 1]);
149+
expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([
123150
'bar_stacked',
124151
'area_stacked',
125152
'area',
@@ -156,7 +183,51 @@ describe('xy_suggestions', () => {
156183
});
157184

158185
expect(suggestions).toHaveLength(visualizationTypes.length);
159-
expect(suggestions.map(({ state }) => state.preferredSeriesType)).toEqual([
186+
expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([
187+
'line',
188+
'bar',
189+
'bar_horizontal',
190+
'bar_stacked',
191+
'bar_horizontal_stacked',
192+
'area',
193+
'area_stacked',
194+
]);
195+
});
196+
197+
test('suggests all basic x y charts when switching from another x y chart with multiple layers', () => {
198+
(generateId as jest.Mock).mockReturnValueOnce('aaa');
199+
const suggestions = getSuggestions({
200+
table: {
201+
isMultiRow: true,
202+
columns: [numCol('bytes'), dateCol('date')],
203+
layerId: 'first',
204+
changeType: 'unchanged',
205+
},
206+
keptLayerIds: ['first', 'second'],
207+
state: {
208+
legend: { isVisible: true, position: 'bottom' },
209+
preferredSeriesType: 'bar',
210+
layers: [
211+
{
212+
layerId: 'first',
213+
seriesType: 'bar',
214+
xAccessor: 'date',
215+
accessors: ['bytes'],
216+
splitAccessor: undefined,
217+
},
218+
{
219+
layerId: 'second',
220+
seriesType: 'bar',
221+
xAccessor: undefined,
222+
accessors: [],
223+
splitAccessor: undefined,
224+
},
225+
],
226+
},
227+
});
228+
229+
expect(suggestions).toHaveLength(visualizationTypes.length);
230+
expect(suggestions.map(({ state }) => xyVisualization.getVisualizationTypeId(state))).toEqual([
160231
'line',
161232
'bar',
162233
'bar_horizontal',
@@ -165,6 +236,15 @@ describe('xy_suggestions', () => {
165236
'area',
166237
'area_stacked',
167238
]);
239+
expect(suggestions.map(({ state }) => state.layers.map((l) => l.layerId))).toEqual([
240+
['first', 'second'],
241+
['first', 'second'],
242+
['first', 'second'],
243+
['first', 'second'],
244+
['first', 'second'],
245+
['first', 'second'],
246+
['first', 'second'],
247+
]);
168248
});
169249

170250
test('suggests all basic x y chart with date on x', () => {
@@ -388,7 +468,7 @@ describe('xy_suggestions', () => {
388468
changeType: 'unchanged',
389469
},
390470
state: currentState,
391-
keptLayerIds: [],
471+
keptLayerIds: ['first'],
392472
});
393473

394474
expect(rest).toHaveLength(visualizationTypes.length - 2);
@@ -497,7 +577,7 @@ describe('xy_suggestions', () => {
497577
changeType: 'extended',
498578
},
499579
state: currentState,
500-
keptLayerIds: [],
580+
keptLayerIds: ['first'],
501581
});
502582

503583
expect(rest).toHaveLength(0);
@@ -536,7 +616,7 @@ describe('xy_suggestions', () => {
536616
changeType: 'reorder',
537617
},
538618
state: currentState,
539-
keptLayerIds: [],
619+
keptLayerIds: ['first'],
540620
});
541621

542622
expect(rest).toHaveLength(0);
@@ -576,7 +656,7 @@ describe('xy_suggestions', () => {
576656
changeType: 'extended',
577657
},
578658
state: currentState,
579-
keptLayerIds: [],
659+
keptLayerIds: ['first'],
580660
});
581661

582662
expect(rest).toHaveLength(0);

x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,17 +394,25 @@ function buildSuggestion({
394394
: undefined,
395395
};
396396

397+
// Maintain consistent order for any layers that were saved
397398
const keptLayers = currentState
398-
? currentState.layers.filter(
399-
(layer) => layer.layerId !== layerId && keptLayerIds.includes(layer.layerId)
400-
)
399+
? currentState.layers
400+
// Remove layers that aren't being suggested
401+
.filter((layer) => keptLayerIds.includes(layer.layerId))
402+
// Update in place
403+
.map((layer) => (layer.layerId === layerId ? newLayer : layer))
404+
// Replace the seriesType on all previous layers
405+
.map((layer) => ({
406+
...layer,
407+
seriesType,
408+
}))
401409
: [];
402410

403411
const state: State = {
404412
legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right },
405413
fittingFunction: currentState?.fittingFunction || 'None',
406414
preferredSeriesType: seriesType,
407-
layers: [...keptLayers, newLayer],
415+
layers: Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer],
408416
};
409417

410418
return {

x-pack/test/functional/apps/lens/smokescreen.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,5 +165,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
165165
// legend item(s), so we're using a class selector here.
166166
expect(await find.allByCssSelector('.echLegendItem')).to.have.length(3);
167167
});
168+
169+
it('should switch from a multi-layer stacked bar to a multi-layer line chart', async () => {
170+
await PageObjects.visualize.navigateToNewVisualization();
171+
await PageObjects.visualize.clickVisType('lens');
172+
await PageObjects.lens.goToTimeRange();
173+
174+
await PageObjects.lens.configureDimension({
175+
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
176+
operation: 'date_histogram',
177+
field: '@timestamp',
178+
});
179+
180+
await PageObjects.lens.configureDimension({
181+
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
182+
operation: 'avg',
183+
field: 'bytes',
184+
});
185+
186+
await PageObjects.lens.createLayer();
187+
188+
expect(await PageObjects.lens.hasChartSwitchWarning('line')).to.eql(false);
189+
190+
await PageObjects.lens.switchToVisualization('line');
191+
192+
expect(await PageObjects.lens.getLayerCount()).to.eql(2);
193+
});
168194
});
169195
}

x-pack/test/functional/page_objects/lens_page.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,5 +167,33 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
167167
await testSubjects.existOrFail('visTypeTitle');
168168
});
169169
},
170+
171+
/**
172+
* Checks a specific subvisualization in the chart switcher for a "data loss" indicator
173+
*
174+
* @param subVisualizationId - the ID of the sub-visualization to switch to, such as
175+
* lnsDatatable or bar_stacked
176+
*/
177+
async hasChartSwitchWarning(subVisualizationId: string) {
178+
await this.openChartSwitchPopover();
179+
180+
const element = await testSubjects.find(`lnsChartSwitchPopover_${subVisualizationId}`);
181+
return await testSubjects.descendantExists('euiKeyPadMenuItem__betaBadgeWrapper', element);
182+
},
183+
184+
/**
185+
* Returns the number of layers visible in the chart configuration
186+
*/
187+
async getLayerCount() {
188+
const elements = await testSubjects.findAll('lnsLayerRemove');
189+
return elements.length;
190+
},
191+
192+
/**
193+
* Adds a new layer to the chart, fails if the chart does not support new layers
194+
*/
195+
async createLayer() {
196+
await testSubjects.click('lnsLayerAddButton');
197+
},
170198
});
171199
}

0 commit comments

Comments
 (0)