Skip to content

Commit 4be3f50

Browse files
nickofthymekibanamachinemacroscopeapp[bot]
authored
[Lens] Improve transform validation checking (elastic#243732)
## Summary This PR is meant to improve the Lens transform validation framework. Closes elastic#242663 Closes elastic#267582 Currently there are a few main problems: - It's verbose when using over and over for the same chart - It's split from the api-to-state validator - It does not ever compare with the initial state when checking state-to-api. The first 2 are fixed but cleaning up the methods calls. ```ts // The old way ❌ validateConverter(simpleMetricAttributes, metricStateSchema); validateAPIConverter(simpleMetricApi, metricStateSchema); // The new way ✅ validator.metric.fromState(simpleMetricAttributes); validator.metric.fromApi(simpleMetricApi); ``` The 3rd issue, the biggest problem is that original configurations can be completely wiped away and the test with still pass. The solution before was to just test the transforms starting with the api then going to state and back with `validateAPIConverter`. However, many of the examples we have, like those from integrations, start from the SO. The idea is to have a `normalizers` for each chart type that [canonicalizes](https://grokipedia.com/page/Canonicalization) or normalizes the original SO into a form that is comparable to the new SO from the config builder output. This means: - Replacing uuids with their respective human-readable ids - Stripping or adding defaults - Cleaning up properties. Mainly omitting `undefined` values instead of explicitly setting the property (i.e. `"filter": undefined`). Initially, this will be an **opt-in** parameter on the `validator.fromState` method, then enforced always when all charts are stabilized. ```ts validator.metric.fromState(simpleMetricAttributes, true); // strict checking enabled ``` In a way this is a bit overkill but I think it is worth it to be able to accurately test input to output 1 to 1, 🍎 to 🍏. This normalization is does per chart with common logic shared between them. The PR starts with the heatmap charts. The framework allows both mutating the original SO and the transformed SO (original SO -> api -> transformed SO) to make it easier to handle per case changes. For example converting the original random ids to the human readable ids is easier than vice versa. There is also an `ignore` option to completely ignore comparing given fields, this should be the last case as it's better to normalize to establish the baseline to compare, rather than ignore it entirely. But in some cases it's just too complex to do so. We can compose these normalizers together for a given chart type to include common normalizers but also to group normalizers but issue, type, context, etc.. In the end joining them together with `mergeNormalizers`. ```ts const alignLegacyTypes: NormalizerConfig<HeatmapAttributes> = { original: (attributes) => { // mutate original attributes as needed. attributes.state.visualization.legend.isVisible = isVisible ?? true; return attributes; }, transformed: (attributes) => { // mutate transformed attributes as needed. return attributes; }, ignore: ['state.datasourceStates.formBased.layers.*.columns.*.params'] } export const normalizeHeatmap = mergeNormalizers([alignLegacyTypes]) ``` > [!IMPORTANT] > The normalizers can mutate the `attributes` directly to make it easier to tweak and this is just test code so I don't really care to get fancy. Also the `attributes` should generally not be shared between tests so there should be no interference. These normalizers **only** apply to the `fromState` validator and only apply when `strict` mode is set to `true` when comparing the original and transformed states. ### Examples The simplest case is we just compare what we put into the CB against the SO that came out of the CB (SO -> API -> SO). <details><summary>Simple Metric</summary> <p> ```diff - Expected - 18 + Received + 14 @@ -1,61 +1,57 @@ Object { "description": "", "references": Array [ Object { "id": "90943e30-9a47-11e8-b64d-95841ca0b247", - "name": "indexpattern-datasource-layer-2821bd27-b805-4dea-a7d4-123c248e63b1", + "name": "indexpattern-datasource-layer-layer_0", "type": "index-pattern", }, ], "state": Object { "adHocDataViews": Object {}, "datasourceStates": Object { "formBased": Object { "layers": Object { - "2821bd27-b805-4dea-a7d4-123c248e63b1": Object { + "layer_0": Object { "columnOrder": Array [ - "812a7944-731e-4967-8b84-1c8bba4ff04b", + "metric_formula_accessor_metric", ], "columns": Object { - "812a7944-731e-4967-8b84-1c8bba4ff04b": Object { + "metric_formula_accessor_metric": Object { + "customLabel": false, "dataType": "number", + "filter": undefined, "isBucketed": false, - "label": "Count of records", + "label": "", "operationType": "count", "params": Object { "emptyAsNull": true, }, "sourceField": "___records___", }, }, - "incompleteColumns": Object {}, + "ignoreGlobalFilters": false, "sampling": 1, - }, - }, }, - "indexpattern": Object { - "layers": Object {}, }, - "textBased": Object { - "layers": Object {}, }, }, "filters": Array [], "internalReferences": Array [], "query": Object { "language": "kuery", "query": "", }, "visualization": Object { - "layerId": "2821bd27-b805-4dea-a7d4-123c248e63b1", + "collapseFn": undefined, + "layerId": "layer_0", "layerType": "data", - "metricAccessor": "812a7944-731e-4967-8b84-1c8bba4ff04b", - "secondaryLabelPosition": "before", - "secondaryTrend": Object { - "type": "none", - }, + "metricAccessor": "metric_formula_accessor_metric", + "showBar": false, + "subtitle": "", + "valueFontMode": "default", }, }, "title": "Lens Metric - By Ref", "version": 1, "visualizationType": "lnsMetric", ``` > Notice many things align but apart from the ids, only a few things are mismatched. </p> </details> If we replace all the uuids with their new respective human-readable ids, the true issues become more apparent... <details><summary>Simple Metric with ids replaced</summary> <p> ```diff - Expected - 12 + Received + 8 @@ -16,46 +16,42 @@ "columnOrder": Array [ "metric_formula_accessor_metric", ], "columns": Object { "metric_formula_accessor_metric": Object { + "customLabel": false, "dataType": "number", + "filter": undefined, "isBucketed": false, - "label": "Count of records", + "label": "", "operationType": "count", "params": Object { "emptyAsNull": true, }, "sourceField": "___records___", }, }, - "incompleteColumns": Object {}, + "ignoreGlobalFilters": false, "sampling": 1, }, - }, - }, - "indexpattern": Object { - "layers": Object {}, }, - "textBased": Object { - "layers": Object {}, }, }, "filters": Array [], "internalReferences": Array [], "query": Object { "language": "kuery", "query": "", }, "visualization": Object { + "collapseFn": undefined, "layerId": "layer_0", "layerType": "data", "metricAccessor": "metric_formula_accessor_metric", - "secondaryLabelPosition": "before", - "secondaryTrend": Object { - "type": "none", - }, + "showBar": false, + "subtitle": "", + "valueFontMode": "default", }, }, "title": "Lens Metric - By Ref", "version": 1, "visualizationType": "lnsMetric", ``` </p> </details> Going one step further and cleaning up both the common state and the chart-specific visualization state, we see true issues, in this case that the label is cleared and some default options are being applied. We can ignore default options on a per-chart basis while still getting most of the benefits of the comparison. <details><summary>Simple Metric with ids replaced</summary> <p> ```diff - Expected - 12 + Received + 8 @@ -16,46 +16,42 @@ "columnOrder": Array [ "metric_formula_accessor_metric", ], "columns": Object { "metric_formula_accessor_metric": Object { + "customLabel": false, "dataType": "number", "isBucketed": false, - "label": "Count of records", + "label": "", "operationType": "count", "params": Object { "emptyAsNull": true, }, "sourceField": "___records___", }, }, "ignoreGlobalFilters": false, "sampling": 1, }, }, }, "filters": Array [], "internalReferences": Array [], "query": Object { "language": "kuery", "query": "", }, "visualization": Object { "layerId": "layer_0", "layerType": "data", "metricAccessor": "metric_formula_accessor_metric", - "secondaryLabelPosition": "before", - "secondaryTrend": Object { - "type": "none", - }, + "showBar": false, + "subtitle": "", + "valueFontMode": "default", }, }, "title": "Lens Metric - By Ref", "version": 1, "visualizationType": "lnsMetric", ``` </p> </details> ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [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 - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: macroscopeapp[bot] <170038800+macroscopeapp[bot]@users.noreply.github.com>
1 parent feb1371 commit 4be3f50

45 files changed

Lines changed: 2282 additions & 840 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

integrations

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 5a5d30c6ebf4fef2c4226a67c31d772ceb93230d

src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/config_builder.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ import {
5353
fromAPItoLensState as fromDatatableAPItoLensState,
5454
fromLensStateToAPI as fromDatatableLensStateToAPI,
5555
} from './transforms/charts/datatable';
56-
import type { LensApiConfig } from './schema';
56+
import type { LensApiConfig, LensApiConfigChartType } from './schema';
5757
import { filtersAndQueryToApiFormat, filtersAndQueryToLensState } from './transforms/utils';
5858
import { isLensLegacyFormat } from './utils';
5959

60-
const compatibilityMap: Record<string, string> = {
60+
const compatibilityMap: Record<string, LensApiConfigChartType> = {
6161
lnsMetric: 'metric',
6262
lnsLegacyMetric: 'legacy_metric',
6363
lnsXY: 'xy',
@@ -172,6 +172,14 @@ export class LensConfigBuilder {
172172
return type in this.apiConvertersByChart;
173173
}
174174

175+
getCompatibleType(type: string): LensApiConfigChartType {
176+
const compatType = compatibilityMap[type];
177+
178+
if (compatType) return compatType;
179+
180+
throw new Error(`No compatible type found for type: ${type}`);
181+
}
182+
175183
getType<C extends ChartTypeLike>(config: C): string | undefined | null {
176184
if (config == null) {
177185
return null;

src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/charts/heatmap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ import {
3030
import { builderEnums } from '../enums';
3131
import { bucketOperationDefinitionSchema } from '../bucket_ops';
3232
import { objectUnion } from './utils/object_union';
33+
import { positionSchema } from '../alignments';
3334

3435
const legendSchemaProps = {
3536
truncate_after_lines: legendTruncateAfterLinesSchema,
3637
visibility: baseLegendVisibilitySchema,
38+
position: schema.maybe(positionSchema()),
3739
size: legendSizeSchema,
3840
};
3941

src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/schema/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,20 @@ export type LensApiAllOperations =
246246
| LensApiBucketOperations
247247
| LensApiStaticValueOperation;
248248

249+
/**
250+
* Supported chart types in the Lens API
251+
*
252+
* @note snake cased
253+
*/
254+
export type LensApiConfigChartType = LensApiConfig['type'];
255+
256+
/**
257+
* Map of Lens API state types to their corresponding config type
258+
*/
259+
export type LensApiConfigByType = {
260+
[K in LensApiConfig['type']]: Extract<LensApiConfig, { type: K }>;
261+
};
262+
249263
export {
250264
// Combined schemas
251265
metricConfigSchema,

src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/datatable/datatable.test.ts

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99

1010
import { AS_CODE_DATA_VIEW_SPEC_TYPE } from '@kbn/as-code-data-views-schema';
1111

12-
import { datatableConfigSchema } from '../../schema';
1312
import type { DatatableConfig } from '../../schema';
1413
import { AUTO_COLOR } from '../../schema/color';
1514
import { LensConfigBuilder } from '../../config_builder';
16-
import { validateAPIConverter, validateConverter } from '../validate';
15+
import { validator } from '../utils/validator';
1716
import {
1817
singleMetricDatatableAttributes,
1918
singleMetricRowSplitDatatableAttributes,
@@ -48,105 +47,103 @@ import {
4847
} from './lens_api_config_esql.mock';
4948

5049
describe('Datatable', () => {
51-
describe('validateConverter', () => {
50+
describe('state transform validation', () => {
5251
it('should convert a datatable chart with single metric column', () => {
53-
validateConverter(singleMetricDatatableAttributes, datatableConfigSchema);
52+
validator.data_table.fromState(singleMetricDatatableAttributes);
5453
});
5554

5655
it('should convert a datatable chart with single metric, row, split by columns', () => {
57-
validateConverter(singleMetricRowSplitDatatableAttributes, datatableConfigSchema);
56+
validator.data_table.fromState(singleMetricRowSplitDatatableAttributes);
5857
});
5958

6059
it('should convert a datatable chart with multiple metrics, rows, split by columns', () => {
61-
validateConverter(multiMetricRowSplitDatatableAttributes, datatableConfigSchema);
60+
validator.data_table.fromState(multiMetricRowSplitDatatableAttributes);
6261
});
6362

6463
it('should convert a datatable chart with full config', () => {
65-
validateConverter(fullConfigDatatableAttributes, datatableConfigSchema);
64+
validator.data_table.fromState(fullConfigDatatableAttributes);
6665
});
6766

6867
it('should convert a datatable chart sorted by a transposed metric column', () => {
69-
validateConverter(sortedByTransposedMetricColumnDatatableAttributes, datatableConfigSchema);
68+
validator.data_table.fromState(sortedByTransposedMetricColumnDatatableAttributes);
7069
});
7170

7271
it('should convert a datatable chart sorted by a row', () => {
73-
validateConverter(sortedByRowDatatableAttributes, datatableConfigSchema);
72+
validator.data_table.fromState(sortedByRowDatatableAttributes);
7473
});
7574

7675
it('should convert an ESQL datatable chart with single metric column', () => {
77-
validateConverter(singleMetricESQLDatatableAttributes, datatableConfigSchema);
76+
validator.data_table.fromState(singleMetricESQLDatatableAttributes);
7877
});
7978

8079
it('should convert an ESQL datatable chart with single metric, row, split by columns', () => {
81-
validateConverter(singleMetricRowSplitESQLDatatableAttributes, datatableConfigSchema);
80+
validator.data_table.fromState(singleMetricRowSplitESQLDatatableAttributes);
8281
});
8382

8483
it('should convert an ESQL datatable chart with multiple metrics, rows, split by columns', () => {
85-
validateConverter(multipleMetricRowSplitESQLDatatableAttributes, datatableConfigSchema);
84+
validator.data_table.fromState(multipleMetricRowSplitESQLDatatableAttributes);
8685
});
8786

8887
it('should convert an ESQL datatable chart with full config', () => {
89-
validateConverter(fullConfigESQLDatatableAttributes, datatableConfigSchema);
88+
validator.data_table.fromState(fullConfigESQLDatatableAttributes);
9089
});
9190

9291
it('should convert an ESQL datatable chart sorted by a transposed metric column', () => {
93-
validateConverter(
94-
sortedByTransposedMetricColumnESQLDatatableAttributes,
95-
datatableConfigSchema
96-
);
92+
validator.data_table.fromState(sortedByTransposedMetricColumnESQLDatatableAttributes);
9793
});
9894

9995
it('should convert a default color by value palette', () => {
100-
validateConverter(defaultColorByValueAttributes, datatableConfigSchema);
96+
validator.data_table.fromState(defaultColorByValueAttributes);
10197
});
10298

10399
it('should convert a selector color by value palette', () => {
104-
validateConverter(selectorColorByValueAttributes, datatableConfigSchema);
100+
validator.data_table.fromState(selectorColorByValueAttributes);
105101
});
106102
});
107-
describe('validateAPIConverter ', () => {
103+
104+
describe('api transform validation', () => {
108105
it('should convert a datatable chart with single metric column', () => {
109-
validateAPIConverter(singleMetricDatatableWithAdhocDataView, datatableConfigSchema);
106+
validator.data_table.fromApi(singleMetricDatatableWithAdhocDataView);
110107
});
111108

112109
it('should convert a datatable chart with multiple metrics, rows, split by columns', () => {
113-
validateAPIConverter(multiMetricRowSplitByDatatableWithAdhocDataView, datatableConfigSchema);
110+
validator.data_table.fromApi(multiMetricRowSplitByDatatableWithAdhocDataView);
114111
});
115112

116113
it('should convert a datatable chart with full config and ad hoc dataView', () => {
117-
validateAPIConverter(fullConfigDatatableWithAdhocDataView, datatableConfigSchema);
114+
validator.data_table.fromApi(fullConfigDatatableWithAdhocDataView);
118115
});
119116

120117
it('should convert a datatable chart with full config and dataView', () => {
121-
validateAPIConverter(fullConfigDatatableWithDataView, datatableConfigSchema);
118+
validator.data_table.fromApi(fullConfigDatatableWithDataView);
122119
});
123120

124121
it('should convert a datatable chart sorted by a transposed column', () => {
125-
validateAPIConverter(sortedByPivotedMetricColumnDatatable, datatableConfigSchema);
122+
validator.data_table.fromApi(sortedByPivotedMetricColumnDatatable);
126123
});
127124

128125
it('should convert a datatable chart sorted by a row column', () => {
129-
validateAPIConverter(sortedByRowDatatable, datatableConfigSchema);
126+
validator.data_table.fromApi(sortedByRowDatatable);
130127
});
131128

132129
it('should convert an ESQL datatable chart with single metric column', () => {
133-
validateAPIConverter(singleMetricESQLDatatable, datatableConfigSchema);
130+
validator.data_table.fromApi(singleMetricESQLDatatable);
134131
});
135132

136133
it('should convert an ESQL datatable chart with multiple metrics, rows, split by columns', () => {
137-
validateAPIConverter(multipleMetricRowSplitESQLDatatable, datatableConfigSchema);
134+
validator.data_table.fromApi(multipleMetricRowSplitESQLDatatable);
138135
});
139136

140137
it('should convert an ESQL datatable chart with full config', () => {
141-
validateAPIConverter(fullConfigESQLDatatable, datatableConfigSchema);
138+
validator.data_table.fromApi(fullConfigESQLDatatable);
142139
});
143140

144141
it('should convert an ESQL datatable chart sorted by a transposed column', () => {
145-
validateAPIConverter(sortedByPivotedMetricColumnESQLDatatable, datatableConfigSchema);
142+
validator.data_table.fromApi(sortedByPivotedMetricColumnESQLDatatable);
146143
});
147144

148145
it('should convert an ESQL datatable chart sorted by a row column', () => {
149-
validateAPIConverter(sortedByRowColumnESQLDatatable, datatableConfigSchema);
146+
validator.data_table.fromApi(sortedByRowColumnESQLDatatable);
150147
});
151148
});
152149

src/platform/packages/shared/kbn-lens-embeddable-utils/config_builder/tests/gauge/gauge.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99

1010
import { AS_CODE_DATA_VIEW_SPEC_TYPE } from '@kbn/as-code-data-views-schema';
1111

12-
import { gaugeConfigSchema } from '../../schema/charts/gauge';
12+
import { validator } from '../utils/validator';
1313
import type { GaugeConfig } from '../../schema/charts/gauge';
1414
import { AUTO_COLOR, NO_COLOR } from '../../schema/color';
1515
import { LensConfigBuilder } from '../../config_builder';
16-
import { validateAPIConverter, validateConverter } from '../validate';
1716
import {
1817
basicGaugeWithAdHocDataView,
1918
basicGaugeWithDataView,
@@ -31,50 +30,51 @@ import {
3130
} from './lens_state_config.mock';
3231

3332
describe('Gauge', () => {
34-
describe('validateConverter', () => {
33+
describe('state transform validation', () => {
3534
it('should convert a gauge chart with full config and absolute color mode', () => {
36-
validateConverter(gaugeAttributes, gaugeConfigSchema);
35+
validator.gauge.fromState(gaugeAttributes);
3736
});
3837

3938
it('should convert a gauge chart with full config and percentage color mode', () => {
40-
validateConverter(gaugeAttributesWithPercentageColorMode, gaugeConfigSchema);
39+
validator.gauge.fromState(gaugeAttributesWithPercentageColorMode);
4140
});
4241

43-
it('should convert a gauge chart with ESQL datasource', () => {
44-
validateConverter(gaugeESQLAttributes, gaugeConfigSchema);
42+
it('should convert a gauge chart with ESQL dataset', () => {
43+
validator.gauge.fromState(gaugeESQLAttributes);
4544
});
4645

4746
it('should convert a default color by value palette', () => {
48-
validateConverter(defaultColorByValueAttributes, gaugeConfigSchema);
47+
validator.gauge.fromState(defaultColorByValueAttributes);
4948
});
5049

5150
it('should convert a selector color by value palette', () => {
52-
validateConverter(selectorColorByValueAttributes, gaugeConfigSchema);
51+
validator.gauge.fromState(selectorColorByValueAttributes);
5352
});
5453
});
55-
describe('validateAPIConverter', () => {
54+
55+
describe('api transform validation', () => {
5656
it('should convert a basic gauge chart with ad hoc dataView', () => {
57-
validateAPIConverter(basicGaugeWithAdHocDataView, gaugeConfigSchema);
57+
validator.gauge.fromApi(basicGaugeWithAdHocDataView);
5858
});
5959

6060
it('should convert a basic gauge chart with dataView', () => {
61-
validateAPIConverter(basicGaugeWithDataView, gaugeConfigSchema);
61+
validator.gauge.fromApi(basicGaugeWithDataView);
6262
});
6363

6464
it('should convert a ESQL-based gauge chart', () => {
65-
validateAPIConverter(esqlGauge, gaugeConfigSchema);
65+
validator.gauge.fromApi(esqlGauge);
6666
});
6767

6868
it('should convert a comprehensive gauge chart with ad hoc data view', () => {
69-
validateAPIConverter(comprehensiveGaugeWithAdHocDataView, gaugeConfigSchema);
69+
validator.gauge.fromApi(comprehensiveGaugeWithAdHocDataView);
7070
});
7171

7272
it('should convert a comprehensive gauge chart with data view', () => {
73-
validateAPIConverter(comprehensiveGaugeWithDataView, gaugeConfigSchema);
73+
validator.gauge.fromApi(comprehensiveGaugeWithDataView);
7474
});
7575

7676
it('should convert a comprehensive ESQL-based gauge chart', () => {
77-
validateAPIConverter(comprehensiveEsqlGauge, gaugeConfigSchema);
77+
validator.gauge.fromApi(comprehensiveEsqlGauge);
7878
});
7979
});
8080

0 commit comments

Comments
 (0)