Skip to content

Commit f461df2

Browse files
[7.x] [Lens] Visualization validation and better error messages (#81439) (#82811)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent b8f2342 commit f461df2

File tree

17 files changed

+1220
-119
lines changed

17 files changed

+1220
-119
lines changed

x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,20 +357,77 @@ describe('Datatable Visualization', () => {
357357
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
358358
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
359359
dataType: 'string',
360-
isBucketed: true,
360+
isBucketed: false, // <= make them metrics
361361
label: 'label',
362362
});
363363

364364
const expression = datatableVisualization.toExpression(
365365
{ layers: [layer] },
366366
frame.datasourceLayers
367367
) as Ast;
368+
368369
const tableArgs = buildExpression(expression).findFunction('lens_datatable_columns');
369370

370371
expect(tableArgs).toHaveLength(1);
371372
expect(tableArgs[0].arguments).toEqual({
372373
columnIds: ['c', 'b'],
373374
});
374375
});
376+
377+
it('returns no expression if the metric dimension is not defined', () => {
378+
const datasource = createMockDatasource('test');
379+
const layer = { layerId: 'a', columns: ['b', 'c'] };
380+
const frame = mockFrame();
381+
frame.datasourceLayers = { a: datasource.publicAPIMock };
382+
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
383+
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
384+
dataType: 'string',
385+
isBucketed: true, // move it from the metric to the break down by side
386+
label: 'label',
387+
});
388+
389+
const expression = datatableVisualization.toExpression(
390+
{ layers: [layer] },
391+
frame.datasourceLayers
392+
);
393+
394+
expect(expression).toEqual(null);
395+
});
396+
});
397+
398+
describe('#getErrorMessages', () => {
399+
it('returns undefined if the datasource is missing a metric dimension', () => {
400+
const datasource = createMockDatasource('test');
401+
const layer = { layerId: 'a', columns: ['b', 'c'] };
402+
const frame = mockFrame();
403+
frame.datasourceLayers = { a: datasource.publicAPIMock };
404+
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
405+
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
406+
dataType: 'string',
407+
isBucketed: true, // move it from the metric to the break down by side
408+
label: 'label',
409+
});
410+
411+
const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame);
412+
413+
expect(error).not.toBeDefined();
414+
});
415+
416+
it('returns undefined if the metric dimension is defined', () => {
417+
const datasource = createMockDatasource('test');
418+
const layer = { layerId: 'a', columns: ['b', 'c'] };
419+
const frame = mockFrame();
420+
frame.datasourceLayers = { a: datasource.publicAPIMock };
421+
datasource.publicAPIMock.getTableSpec.mockReturnValue([{ columnId: 'c' }, { columnId: 'b' }]);
422+
datasource.publicAPIMock.getOperationForColumnId.mockReturnValue({
423+
dataType: 'string',
424+
isBucketed: false, // keep it a metric
425+
label: 'label',
426+
});
427+
428+
const error = datatableVisualization.getErrorMessages({ layers: [layer] }, frame);
429+
430+
expect(error).not.toBeDefined();
431+
});
375432
});
376433
});

x-pack/plugins/lens/public/datatable_visualization/visualization.tsx

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66

77
import { Ast } from '@kbn/interpreter/common';
88
import { i18n } from '@kbn/i18n';
9-
import { SuggestionRequest, Visualization, VisualizationSuggestion, Operation } from '../types';
9+
import {
10+
SuggestionRequest,
11+
Visualization,
12+
VisualizationSuggestion,
13+
Operation,
14+
DatasourcePublicAPI,
15+
} from '../types';
1016
import { LensIconChartDatatable } from '../assets/chart_datatable';
1117

1218
export interface LayerState {
@@ -128,16 +134,13 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
128134
},
129135

130136
getConfiguration({ state, frame, layerId }) {
131-
const layer = state.layers.find((l) => l.layerId === layerId);
132-
if (!layer) {
137+
const { sortedColumns, datasource } =
138+
getDataSourceAndSortedColumns(state, frame.datasourceLayers, layerId) || {};
139+
140+
if (!sortedColumns) {
133141
return { groups: [] };
134142
}
135143

136-
const datasource = frame.datasourceLayers[layer.layerId];
137-
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
138-
// When we add a column it could be empty, and therefore have no order
139-
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
140-
141144
return {
142145
groups: [
143146
{
@@ -146,7 +149,9 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
146149
defaultMessage: 'Break down by',
147150
}),
148151
layerId: state.layers[0].layerId,
149-
accessors: sortedColumns.filter((c) => datasource.getOperationForColumnId(c)?.isBucketed),
152+
accessors: sortedColumns.filter(
153+
(c) => datasource!.getOperationForColumnId(c)?.isBucketed
154+
),
150155
supportsMoreColumns: true,
151156
filterOperations: (op) => op.isBucketed,
152157
dataTestSubj: 'lnsDatatable_column',
@@ -158,7 +163,7 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
158163
}),
159164
layerId: state.layers[0].layerId,
160165
accessors: sortedColumns.filter(
161-
(c) => !datasource.getOperationForColumnId(c)?.isBucketed
166+
(c) => !datasource!.getOperationForColumnId(c)?.isBucketed
162167
),
163168
supportsMoreColumns: true,
164169
filterOperations: (op) => !op.isBucketed,
@@ -194,14 +199,19 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
194199
};
195200
},
196201

197-
toExpression(state, datasourceLayers, { title, description } = {}): Ast {
198-
const layer = state.layers[0];
199-
const datasource = datasourceLayers[layer.layerId];
200-
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
201-
// When we add a column it could be empty, and therefore have no order
202-
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
203-
const operations = sortedColumns
204-
.map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) }))
202+
toExpression(state, datasourceLayers, { title, description } = {}): Ast | null {
203+
const { sortedColumns, datasource } =
204+
getDataSourceAndSortedColumns(state, datasourceLayers, state.layers[0].layerId) || {};
205+
206+
if (
207+
sortedColumns?.length &&
208+
sortedColumns.filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed).length === 0
209+
) {
210+
return null;
211+
}
212+
213+
const operations = sortedColumns!
214+
.map((columnId) => ({ columnId, operation: datasource!.getOperationForColumnId(columnId) }))
205215
.filter((o): o is { columnId: string; operation: Operation } => !!o.operation);
206216

207217
return {
@@ -232,4 +242,24 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
232242
],
233243
};
234244
},
245+
246+
getErrorMessages(state, frame) {
247+
return undefined;
248+
},
235249
};
250+
251+
function getDataSourceAndSortedColumns(
252+
state: DatatableVisualizationState,
253+
datasourceLayers: Record<string, DatasourcePublicAPI>,
254+
layerId: string
255+
) {
256+
const layer = state.layers.find((l: LayerState) => l.layerId === layerId);
257+
if (!layer) {
258+
return undefined;
259+
}
260+
const datasource = datasourceLayers[layer.layerId];
261+
const originalOrder = datasource.getTableSpec().map(({ columnId }) => columnId);
262+
// When we add a column it could be empty, and therefore have no order
263+
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.columns)));
264+
return { datasource, sortedColumns };
265+
}

x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_helpers.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { SavedObjectReference } from 'kibana/public';
88
import { Ast } from '@kbn/interpreter/common';
9-
import { Datasource, DatasourcePublicAPI, Visualization } from '../../types';
9+
import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types';
1010
import { buildExpression } from './expression_helpers';
1111
import { Document } from '../../persistence/saved_object_store';
1212
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
@@ -91,3 +91,29 @@ export async function persistedStateToExpression(
9191
datasourceLayers,
9292
});
9393
}
94+
95+
export const validateDatasourceAndVisualization = (
96+
currentDataSource: Datasource | null,
97+
currentDatasourceState: unknown | null,
98+
currentVisualization: Visualization | null,
99+
currentVisualizationState: unknown | undefined,
100+
frameAPI: FramePublicAPI
101+
):
102+
| Array<{
103+
shortMessage: string;
104+
longMessage: string;
105+
}>
106+
| undefined => {
107+
const datasourceValidationErrors = currentDatasourceState
108+
? currentDataSource?.getErrorMessages(currentDatasourceState)
109+
: undefined;
110+
111+
const visualizationValidationErrors = currentVisualizationState
112+
? currentVisualization?.getErrorMessages(currentVisualizationState, frameAPI)
113+
: undefined;
114+
115+
if (datasourceValidationErrors || visualizationValidationErrors) {
116+
return [...(datasourceValidationErrors || []), ...(visualizationValidationErrors || [])];
117+
}
118+
return undefined;
119+
};

0 commit comments

Comments
 (0)