[Lens] Improvements on the suggestions api (#153931)

## Summary

This is a follow up PR from my PR that enables Lens suggestions in
Discover. Specifically:

- it moves the filtering from unified_histogram to lens api
- it solves a bug that exists atm. If I have a query that it doesn't
return a valid chart (e.g. select <string_field> from index) the chart
is rendered wrongly (we should render nothing at this case). For this
reason we add the incomplete flag in each visualization suggest which is
set to true every time my chart is incomplete (missing required
dimensions)
This commit is contained in:
Stratoula Kalafateli 2023-04-04 14:06:24 +03:00 committed by GitHub
parent 48ded93bc7
commit d718066d75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 166 additions and 26 deletions

View file

@ -36,21 +36,15 @@ export const useLensSuggestions = ({
contextualFields: columns,
query: query && isOfAggregateQueryType(query) ? query : undefined,
};
const lensSuggestions = isPlainRecord ? lensSuggestionsApi(context, dataView) : undefined;
const firstSuggestion = lensSuggestions?.length ? lensSuggestions[0] : undefined;
const restSuggestions = lensSuggestions?.filter((sug) => {
return !sug.hide && sug.visualizationId !== 'lnsLegacyMetric';
});
const firstSuggestionExists = restSuggestions?.find(
(sug) => sug.title === firstSuggestion?.title
);
if (firstSuggestion && !firstSuggestionExists) {
restSuggestions?.push(firstSuggestion);
}
return { firstSuggestion, restSuggestions };
const allSuggestions = isPlainRecord
? lensSuggestionsApi(context, dataView, ['lnsDatatable']) ?? []
: [];
const [firstSuggestion] = allSuggestions;
return { firstSuggestion, allSuggestions };
}, [columns, dataView, isPlainRecord, lensSuggestionsApi, query]);
const [allSuggestions, setAllSuggestions] = useState(suggestions.restSuggestions);
const [allSuggestions, setAllSuggestions] = useState(suggestions.allSuggestions);
const currentSuggestion = originalSuggestion ?? suggestions.firstSuggestion;
const suggestionDeps = useRef(getSuggestionDeps({ dataView, query, columns }));
@ -58,7 +52,7 @@ export const useLensSuggestions = ({
const newSuggestionsDeps = getSuggestionDeps({ dataView, query, columns });
if (!isEqual(suggestionDeps.current, newSuggestionsDeps)) {
setAllSuggestions(suggestions.restSuggestions);
setAllSuggestions(suggestions.allSuggestions);
onSuggestionChange?.(suggestions.firstSuggestion);
suggestionDeps.current = newSuggestionsDeps;
@ -69,15 +63,13 @@ export const useLensSuggestions = ({
onSuggestionChange,
query,
suggestions.firstSuggestion,
suggestions.restSuggestions,
suggestions.allSuggestions,
]);
return {
allSuggestions,
currentSuggestion,
suggestionUnsupported:
isPlainRecord &&
(!currentSuggestion || currentSuggestion?.visualizationId === 'lnsDatatable'),
suggestionUnsupported: isPlainRecord && !currentSuggestion,
};
};

View file

@ -102,4 +102,101 @@ describe('suggestionsApi', () => {
expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
expect(suggestions?.length).toEqual(1);
});
test('filters out legacy metric and incomplete suggestions', async () => {
const dataView = { id: 'index1' } as unknown as DataView;
const visualizationMap = {
testVis: {
...mockVis,
getSuggestions: () => [
{
score: 0.2,
title: 'Test',
state: {},
previewIcon: 'empty',
visualizationId: 'lnsLegacyMetric',
},
{
score: 0.8,
title: 'Test2',
state: {},
previewIcon: 'empty',
},
{
score: 0.8,
title: 'Test2',
state: {},
previewIcon: 'empty',
incomplete: true,
},
],
},
};
datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
generateSuggestion(),
]);
const context = {
dataViewSpec: {
id: 'index1',
title: 'index1',
name: 'DataView',
},
fieldName: '',
contextualFields: ['field1', 'field2'],
query: {
sql: 'SELECT field1, field2 FROM "index1"',
},
};
const suggestions = suggestionsApi({ context, dataView, datasourceMap, visualizationMap });
expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
expect(suggestions?.length).toEqual(1);
});
test('filters out the suggestion if exists on excludedVisualizations', async () => {
const dataView = { id: 'index1' } as unknown as DataView;
const visualizationMap = {
testVis: {
...mockVis,
getSuggestions: () => [
{
score: 0.2,
title: 'Test',
state: {},
previewIcon: 'empty',
visualizationId: 'lnsXY',
},
{
score: 0.8,
title: 'Test2',
state: {},
previewIcon: 'empty',
},
],
},
};
datasourceMap.textBased.getDatasourceSuggestionsForVisualizeField.mockReturnValue([
generateSuggestion(),
]);
const context = {
dataViewSpec: {
id: 'index1',
title: 'index1',
name: 'DataView',
},
fieldName: '',
contextualFields: ['field1', 'field2'],
query: {
sql: 'SELECT field1, field2 FROM "index1"',
},
};
const suggestions = suggestionsApi({
context,
dataView,
datasourceMap,
visualizationMap,
excludedVisualizations: ['lnsXY'],
});
expect(datasourceMap.textBased.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled();
expect(suggestions?.length).toEqual(1);
});
});

View file

@ -15,6 +15,7 @@ interface SuggestionsApi {
dataView: DataView;
visualizationMap?: VisualizationMap;
datasourceMap?: DatasourceMap;
excludedVisualizations?: string[];
}
export const suggestionsApi = ({
@ -22,6 +23,7 @@ export const suggestionsApi = ({
dataView,
datasourceMap,
visualizationMap,
excludedVisualizations,
}: SuggestionsApi) => {
if (!datasourceMap || !visualizationMap || !dataView.id) return undefined;
const datasourceStates = {
@ -63,7 +65,13 @@ export const suggestionsApi = ({
});
if (!suggestions.length) return [];
const activeVisualization = suggestions[0];
// compute the rest suggestions depending on the active one
if (
activeVisualization.incomplete ||
excludedVisualizations?.includes(activeVisualization.visualizationId)
) {
return [];
}
// compute the rest suggestions depending on the active one and filter out the lnsLegacyMetric
const newSuggestions = getSuggestions({
datasourceMap,
datasourceStates: {
@ -76,7 +84,7 @@ export const suggestionsApi = ({
activeVisualization: visualizationMap[activeVisualization.visualizationId],
visualizationState: activeVisualization.visualizationState,
dataViews,
});
}).filter((sug) => !sug.hide && sug.visualizationId !== 'lnsLegacyMetric');
return [activeVisualization, ...newSuggestions];
};

View file

@ -241,7 +241,8 @@ export interface LensPublicStart {
export type LensSuggestionsApi = (
context: VisualizeFieldContext | VisualizeEditorContext,
dataViews: DataView
dataViews: DataView,
excludedVisualizations?: string[]
) => Suggestion[] | undefined;
export class LensPlugin {
@ -607,15 +608,13 @@ export class LensPlugin {
return {
formula: createFormulaPublicApi(),
chartInfo: createChartInfoApi(startDependencies.dataViews, this.editorFrameService),
suggestions: (
context: VisualizeFieldContext | VisualizeEditorContext,
dataView: DataView
) => {
suggestions: (context, dataView, excludedVisualizations) => {
return suggestionsApi({
datasourceMap,
visualizationMap,
context,
dataView,
excludedVisualizations,
});
},
};

View file

@ -874,6 +874,8 @@ export interface Suggestion<T = unknown, V = unknown> {
previewExpression?: Ast | string;
previewIcon: IconType;
hide?: boolean;
// flag to indicate if the visualization is incomplete
incomplete?: boolean;
changeType: TableChangeType;
keptLayerIds: string[];
}
@ -926,6 +928,10 @@ export interface VisualizationSuggestion<T = unknown> {
* directly.
*/
hide?: boolean;
/**
* Flag indicating whether this suggestion is incomplete
*/
incomplete?: boolean;
/**
* Descriptive title of the suggestion. Should be as short as possible. This title is shown if
* the suggestion is advertised to the user and will also show either the `previewExpression` or

View file

@ -158,11 +158,13 @@ describe('shows suggestions', () => {
},
title: 'Gauge',
hide: true,
incomplete: true,
previewIcon: IconChartHorizontalBullet,
score: 0.5,
},
{
hide: true,
incomplete: true,
previewIcon: IconChartVerticalBullet,
title: 'Gauge',
score: 0.5,
@ -208,6 +210,7 @@ describe('shows suggestions', () => {
previewIcon: IconChartVerticalBullet,
title: 'Gauge',
hide: false, // shows suggestion when current is gauge
incomplete: false,
score: 0.5,
},
]);

View file

@ -69,6 +69,7 @@ export const getSuggestions: Visualization<GaugeVisualizationState>['getSuggesti
shape === GaugeShapes.VERTICAL_BULLET ? IconChartVerticalBullet : IconChartHorizontalBullet,
score: 0.5,
hide: !isGauge || state?.metricAccessor === undefined, // only display for gauges for beta
incomplete: state?.metricAccessor === undefined,
};
const suggestions = isGauge

View file

@ -299,6 +299,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@ -352,6 +353,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
incomplete: true,
previewIcon: IconChartHeatmap,
score: 0,
},
@ -405,6 +407,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
incomplete: true,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@ -469,6 +472,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: true,
incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@ -535,6 +539,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.6,
},
@ -609,6 +614,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.3,
},
@ -683,6 +689,7 @@ describe('heatmap suggestions', () => {
},
title: 'Heat map',
hide: false,
incomplete: false,
previewIcon: IconChartHeatmap,
score: 0.9,
},

View file

@ -130,6 +130,7 @@ export const getSuggestions: Visualization<HeatmapVisualizationState>['getSugges
hide,
previewIcon: IconChartHeatmap,
score: Number(score.toFixed(1)),
incomplete: isSingleBucketDimension || isOnlyMetricDimension,
},
];
};

View file

@ -455,6 +455,7 @@ describe('suggestions', () => {
});
expect(currentSuggestions).toHaveLength(5);
expect(currentSuggestions.every((s) => s.hide)).toEqual(true);
expect(currentSuggestions.every((s) => s.incomplete)).toEqual(true);
});
it('should suggest a donut chart as initial state when only one bucket', () => {
@ -1039,6 +1040,7 @@ describe('suggestions', () => {
Array [
Object {
"hide": false,
"incomplete": false,
"previewIcon": [Function],
"score": 0.61,
"state": Object {
@ -1148,6 +1150,7 @@ describe('suggestions', () => {
Array [
Object {
"hide": false,
"incomplete": false,
"previewIcon": [Function],
"score": 0.46,
"state": Object {

View file

@ -330,5 +330,6 @@ export function suggestions({
.map((suggestion) => ({
...suggestion,
hide: shouldHideSuggestion || incompleteConfiguration || suggestion.hide,
incomplete: incompleteConfiguration,
}));
}

View file

@ -149,6 +149,25 @@ describe('xy_suggestions', () => {
);
});
test('marks incomplete as true when no metric is provided', () => {
expect(
(
[
{
isMultiRow: true,
columns: [strCol('foo')],
layerId: 'first',
changeType: 'unchanged',
},
] as TableSuggestion[]
).map((table) => {
const suggestions = getSuggestions({ table, keptLayerIds: [] });
expect(suggestions.every((suggestion) => suggestion.incomplete)).toEqual(true);
expect(suggestions).toHaveLength(10);
})
);
});
test('rejects the configuration when metric isStaticValue', () => {
(generateId as jest.Mock).mockReturnValueOnce('aaa');
const suggestions = getSuggestions({

View file

@ -573,6 +573,8 @@ function buildSuggestion({
existingLayer && Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer],
};
const isIncomplete = yValues.length === 0;
return {
title,
score: getScore(yValues, splitBy, changeType),
@ -583,10 +585,11 @@ function buildSuggestion({
// Don't advertise removing dimensions
(currentState && changeType === 'reduced') ||
// Don't advertise charts without y axis
yValues.length === 0 ||
isIncomplete ||
// Don't advertise charts without at least one split
(!xValue && !splitBy)),
state,
incomplete: isIncomplete,
previewIcon: getIconForSeries(seriesType),
};
}