Skip to content

Commit 26575d6

Browse files
ppisljarkibanamachinedej611
authored
[Lens as Code] partition chart schema (#236058)
## Summary adds schema for partition charts resolves #234555 resolves #234569 resolves #234568 resolves #234566 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: dej611 <dej611@gmail.com> Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
1 parent bae752e commit 26575d6

10 files changed

Lines changed: 1451 additions & 0 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { LENS_EMPTY_AS_NULL_DEFAULT_VALUE } from '../../transforms/columns/utils';
11+
import { mosaicStateSchema } from './mosaic';
12+
13+
describe('Mosaic Schema', () => {
14+
const baseMosaicConfig = {
15+
type: 'mosaic' as const,
16+
dataset: {
17+
type: 'dataView' as const,
18+
id: 'test-data-view',
19+
},
20+
};
21+
22+
const defaultValues = {
23+
sampling: 1,
24+
ignore_global_filters: false,
25+
};
26+
27+
it('validates minimal configuration', () => {
28+
const input = {
29+
...baseMosaicConfig,
30+
metrics: [
31+
{
32+
operation: 'count' as const,
33+
field: 'test_field',
34+
empty_as_null: LENS_EMPTY_AS_NULL_DEFAULT_VALUE,
35+
},
36+
],
37+
group_by: [
38+
{
39+
operation: 'terms' as const,
40+
fields: ['category'],
41+
},
42+
],
43+
};
44+
45+
const validated = mosaicStateSchema.validate(input);
46+
expect(validated).toEqual({
47+
...defaultValues,
48+
...input,
49+
group_by: [{ ...input.group_by[0], size: 5 }],
50+
});
51+
});
52+
53+
it('validates full configuration', () => {
54+
const input = {
55+
...baseMosaicConfig,
56+
title: 'Sales Mosaic',
57+
description: 'Sales data visualization',
58+
metrics: [
59+
{
60+
operation: 'sum' as const,
61+
field: 'sales',
62+
empty_as_null: LENS_EMPTY_AS_NULL_DEFAULT_VALUE,
63+
color: {
64+
type: 'static' as const,
65+
color: '#blue',
66+
},
67+
},
68+
],
69+
group_by: [
70+
{
71+
operation: 'terms' as const,
72+
fields: ['category'],
73+
collapse_by: 'avg' as const,
74+
},
75+
],
76+
legend: {
77+
nested: true,
78+
truncate_after_lines: 5,
79+
visible: 'hide' as const,
80+
size: 'small' as const,
81+
},
82+
value_display: {
83+
mode: 'hidden' as const,
84+
},
85+
};
86+
87+
const validated = mosaicStateSchema.validate(input);
88+
expect(validated).toEqual({
89+
...defaultValues,
90+
...input,
91+
group_by: [{ ...input.group_by[0], size: 5 }],
92+
});
93+
});
94+
95+
it('validates default values are applied', () => {
96+
const input = {
97+
...baseMosaicConfig,
98+
metrics: [
99+
{
100+
operation: 'count' as const,
101+
field: 'test_field',
102+
empty_as_null: LENS_EMPTY_AS_NULL_DEFAULT_VALUE,
103+
},
104+
],
105+
group_by: [
106+
{
107+
operation: 'terms' as const,
108+
fields: ['category'],
109+
},
110+
],
111+
legend: {},
112+
};
113+
114+
const validated = mosaicStateSchema.validate(input);
115+
expect(validated.legend).toEqual({});
116+
});
117+
118+
it('throws on empty group_by array', () => {
119+
const input = {
120+
...baseMosaicConfig,
121+
metrics: [
122+
{
123+
operation: 'count' as const,
124+
field: 'test_field',
125+
},
126+
],
127+
group_by: [],
128+
};
129+
130+
expect(() => mosaicStateSchema.validate(input)).toThrow();
131+
});
132+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { TypeOf } from '@kbn/config-schema';
11+
import { schema } from '@kbn/config-schema';
12+
import {
13+
countMetricOperationSchema,
14+
counterRateOperationSchema,
15+
cumulativeSumOperationSchema,
16+
differencesOperationSchema,
17+
formulaOperationDefinitionSchema,
18+
lastValueOperationSchema,
19+
metricOperationSchema,
20+
movingAverageOperationSchema,
21+
percentileOperationSchema,
22+
percentileRanksOperationSchema,
23+
staticOperationDefinitionSchema,
24+
uniqueCountMetricOperationSchema,
25+
sumMetricOperationSchema,
26+
esqlColumnSchema,
27+
genericOperationOptionsSchema,
28+
} from '../metric_ops';
29+
import { colorByValueSchema, colorMappingSchema, staticColorSchema } from '../color';
30+
import { datasetSchema, datasetEsqlTableSchema } from '../dataset';
31+
import {
32+
bucketDateHistogramOperationSchema,
33+
bucketTermsOperationSchema,
34+
bucketHistogramOperationSchema,
35+
bucketRangesOperationSchema,
36+
bucketFiltersOperationSchema,
37+
} from '../bucket_ops';
38+
import { collapseBySchema, layerSettingsSchema, sharedPanelInfoSchema } from '../shared';
39+
import {
40+
legendNestedSchema,
41+
legendTruncateAfterLinesSchema,
42+
legendVisibleSchema,
43+
legendSizeSchema,
44+
valueDisplaySchema,
45+
} from './partition_shared';
46+
47+
const mosaicStateSharedSchema = {
48+
legend: schema.maybe(
49+
schema.object({
50+
nested: legendNestedSchema,
51+
truncate_after_lines: legendTruncateAfterLinesSchema,
52+
visible: legendVisibleSchema,
53+
size: legendSizeSchema,
54+
})
55+
),
56+
value_display: valueDisplaySchema,
57+
};
58+
59+
const partitionStatePrimaryMetricOptionsSchema = schema.object({
60+
/**
61+
* Color configuration
62+
*/
63+
color: schema.maybe(staticColorSchema),
64+
});
65+
66+
const partitionStateBreakdownByOptionsSchema = schema.object({
67+
/**
68+
* Color configuration
69+
*/
70+
color: schema.maybe(schema.oneOf([colorByValueSchema, colorMappingSchema])),
71+
/**
72+
* Collapse by function. This parameter is used to collapse the
73+
* metric chart when the number of columns is bigger than the
74+
* number of columns specified in the columns parameter.
75+
* Possible values:
76+
* - 'avg': Collapse by average
77+
* - 'sum': Collapse by sum
78+
* - 'max': Collapse by max
79+
* - 'min': Collapse by min
80+
* - 'none': Do not collapse
81+
*/
82+
collapse_by: schema.maybe(collapseBySchema),
83+
});
84+
85+
export const mosaicStateSchemaNoESQL = schema.object({
86+
type: schema.literal('mosaic'),
87+
...sharedPanelInfoSchema,
88+
...layerSettingsSchema,
89+
...datasetSchema,
90+
...mosaicStateSharedSchema,
91+
/**
92+
* Primary value configuration, must define operation.
93+
*/
94+
metrics: schema.arrayOf(
95+
schema.oneOf([
96+
// oneOf allows only 12 items
97+
// so break down metrics based on the type: field-based, reference-based, formula-like
98+
schema.oneOf([
99+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, countMetricOperationSchema]),
100+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, uniqueCountMetricOperationSchema]),
101+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, metricOperationSchema]),
102+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, sumMetricOperationSchema]),
103+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, lastValueOperationSchema]),
104+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, percentileOperationSchema]),
105+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, percentileRanksOperationSchema]),
106+
]),
107+
schema.oneOf([
108+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, differencesOperationSchema]),
109+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, movingAverageOperationSchema]),
110+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, cumulativeSumOperationSchema]),
111+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, counterRateOperationSchema]),
112+
]),
113+
schema.oneOf([
114+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, staticOperationDefinitionSchema]),
115+
schema.allOf([partitionStatePrimaryMetricOptionsSchema, formulaOperationDefinitionSchema]),
116+
]),
117+
]),
118+
{ minSize: 1 }
119+
),
120+
/**
121+
* Configure how to break down the metric (e.g. show one metric per term).
122+
*/
123+
group_by: schema.arrayOf(
124+
schema.maybe(
125+
schema.oneOf([
126+
schema.allOf([partitionStateBreakdownByOptionsSchema, bucketDateHistogramOperationSchema]),
127+
schema.allOf([partitionStateBreakdownByOptionsSchema, bucketTermsOperationSchema]),
128+
schema.allOf([partitionStateBreakdownByOptionsSchema, bucketHistogramOperationSchema]),
129+
schema.allOf([partitionStateBreakdownByOptionsSchema, bucketRangesOperationSchema]),
130+
schema.allOf([partitionStateBreakdownByOptionsSchema, bucketFiltersOperationSchema]),
131+
])
132+
),
133+
{ minSize: 1 }
134+
),
135+
});
136+
137+
const mosaicStateSchemaESQL = schema.object({
138+
type: schema.literal('mosaic'),
139+
...sharedPanelInfoSchema,
140+
...layerSettingsSchema,
141+
...datasetEsqlTableSchema,
142+
...mosaicStateSharedSchema,
143+
/**
144+
* Primary value configuration, must define operation.
145+
*/
146+
metrics: schema.allOf([
147+
schema.object(genericOperationOptionsSchema),
148+
partitionStatePrimaryMetricOptionsSchema,
149+
esqlColumnSchema,
150+
]),
151+
/**
152+
* Configure how to break down the metric (e.g. show one metric per term).
153+
*/
154+
group_by: schema.maybe(schema.allOf([partitionStateBreakdownByOptionsSchema, esqlColumnSchema])),
155+
});
156+
157+
export const mosaicStateSchema = schema.oneOf([mosaicStateSchemaNoESQL, mosaicStateSchemaESQL]);
158+
159+
export type MosaicState = TypeOf<typeof mosaicStateSchema>;
160+
export type MosaicStateNoESQL = TypeOf<typeof mosaicStateSchemaNoESQL>;
161+
export type MosaicStateESQL = TypeOf<typeof mosaicStateSchemaESQL>;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { schema } from '@kbn/config-schema';
11+
12+
export const legendTruncateAfterLinesSchema = schema.maybe(
13+
schema.number({
14+
defaultValue: 1,
15+
min: 1,
16+
max: 10,
17+
meta: { description: 'Maximum lines before truncating legend items (1-10)' },
18+
})
19+
);
20+
21+
export const legendVisibleSchema = schema.maybe(
22+
schema.oneOf([schema.literal('auto'), schema.literal('show'), schema.literal('hide')], {
23+
meta: { description: 'Legend visibility: auto, show, or hide' },
24+
})
25+
);
26+
27+
export const legendSizeSchema = schema.maybe(
28+
schema.oneOf(
29+
[
30+
schema.literal('auto'),
31+
schema.literal('small'),
32+
schema.literal('medium'),
33+
schema.literal('large'),
34+
schema.literal('xlarge'),
35+
],
36+
{ meta: { description: 'Legend size: auto, small, medium, large, or xlarge' } }
37+
)
38+
);
39+
40+
export const valueDisplaySchema = schema.maybe(
41+
schema.object(
42+
{
43+
mode: schema.oneOf(
44+
[schema.literal('hidden'), schema.literal('absolute'), schema.literal('percentage')],
45+
{ meta: { description: 'Value display mode: hidden, absolute, or percentage' } }
46+
),
47+
percent_decimals: schema.maybe(
48+
schema.number({
49+
defaultValue: 2,
50+
min: 0,
51+
max: 10,
52+
meta: { description: 'Decimal places for percentage display (0-10)' },
53+
})
54+
),
55+
},
56+
{ meta: { description: 'Configuration for displaying values in chart cells' } }
57+
)
58+
);
59+
60+
export const legendNestedSchema = schema.maybe(
61+
schema.boolean({
62+
defaultValue: false,
63+
meta: { description: 'Show nested legend with hierarchical breakdown levels' },
64+
})
65+
);

0 commit comments

Comments
 (0)