[Lens] Embeddable overrides feature (#153204)

## Summary

Fixes #149220
Fixes https://github.com/elastic/kibana/issues/148845


This PR introduces the concept of `Embeddable` overrides within the Lens
Embeddable: overrides are a small subset of configuration options that
can overwrite Lens original Elastic Chart rendering configuration.
This feature will only apply at the Embeddable level and is ignored
within the Lens editor context.
The readme has been updated with some documentation on the topic.

### Playground changes

This PR also contains a refactor/enhancement of the playground example
app to showcase the `attributes` and `overrides` for most Lens charts.
The UI has been redesigned with 3 main dropdowns with some explanation
of the different switch controls:

<img width="730" alt="Screenshot 2023-03-16 at 16 01 22"
src="https://user-images.githubusercontent.com/924948/225660824-f40446d7-923b-4b0c-af5a-aacf1c3b3b33.png">
<img width="661" alt="Screenshot 2023-03-16 at 16 01 28"
src="https://user-images.githubusercontent.com/924948/225660841-82ae8428-d003-4379-beea-5eac70662955.png">
<img width="658" alt="Screenshot 2023-03-16 at 16 01 33"
src="https://user-images.githubusercontent.com/924948/225660930-87fce81b-55b2-46fd-bc8b-f4767abc2848.png">

For each override setting an example code snippet is shown on hover:
<img width="387" alt="Screenshot 2023-03-20 at 11 16 52"
src="https://user-images.githubusercontent.com/924948/226362615-46920754-e519-4584-adfd-b7b9c2b5bde6.png">
<img width="388" alt="Screenshot 2023-03-20 at 11 17 00"
src="https://user-images.githubusercontent.com/924948/226362620-8e77cf38-9f2f-48d5-a5eb-f65ad089bc40.png">


The second menu badge shows when overrides are enabled:
<img width="659" alt="Screenshot 2023-03-16 at 16 01 44"
src="https://user-images.githubusercontent.com/924948/225661114-2aaf0c46-2320-41dc-aad1-8e4a26da0635.png">

Different chart types have different options available:
<img width="724" alt="Screenshot 2023-03-16 at 16 04 58"
src="https://user-images.githubusercontent.com/924948/225661241-61899a19-64d3-4326-9645-f908c9b35b65.png">
<img width="653" alt="Screenshot 2023-03-16 at 16 07 42"
src="https://user-images.githubusercontent.com/924948/225661248-3f6e489e-6a0e-4ac6-a0b9-994ed1572750.png">
<img width="635" alt="Screenshot 2023-03-20 at 14 58 00"
src="https://user-images.githubusercontent.com/924948/226362432-76cd8d7d-06de-4a55-a809-9e64e243cd8a.png">


The datatable and metric visualization have no overrides for now.

### Difference with #152842

The two feature work in a similar space, but they are substantially
different from their use cases.

The `overrides` feature is something to use in 2 scenarios:
* small styling/tuning configuration of the final chart via
Lens-unsupported Elastic Chart props
* For instance having `integersOnly` ticks on a XY axis, or value labels
outside only for a pie chart
* Selectively turning off specific event handlers on the component
* For instance to completely remove any complex logic from a legend item
(i.e. filter popup)

The `preventDefault` feature is useful instead when the user wants to
keep all the handlers at chart level, but selectively disabled some
Kibana-wide event from bubble. For instance clicking on a bar or pie
slice should trigger the `edit` event but the consumer's custom handler
should be the only one to be executed, without bubbling up to the
`unifiedSearch` registered triggers.

### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [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
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Stratoula Kalafateli <stratoula1@gmail.com>
Co-authored-by: Drew Tate <drewctate@gmail.com>
This commit is contained in:
Marco Liberati 2023-04-04 12:22:49 +02:00 committed by GitHub
parent 95bc7c0e1c
commit bd48d13e17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
73 changed files with 1793 additions and 302 deletions

View file

@ -6,4 +6,5 @@
* Side Public License, v 1.
*/
export { extractContainerType, extractVisualizationType } from './utils';
export { extractContainerType, extractVisualizationType, getOverridesFor } from './utils';
export type { Simplify, MakeOverridesSerializable } from './types';

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
// Overrides should not expose Functions, React nodes and children props
// So filter out any type which is not serializable
export type MakeOverridesSerializable<T> = {
[KeyType in keyof T]: NonNullable<T[KeyType]> extends Function
? // cannot use boolean here as it would be challenging to distinguish
// between a "native" boolean props and a disabled callback
// so use a specific keyword
'ignore'
: // be careful here to not filter out string/number types
NonNullable<T[KeyType]> extends React.ReactChildren | React.ReactElement
? never
: // make it recursive
NonNullable<T[KeyType]> extends object
? MakeOverridesSerializable<T[KeyType]>
: NonNullable<T[KeyType]>;
};

View file

@ -0,0 +1,33 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getOverridesFor } from './utils';
describe('Overrides utilities', () => {
describe('getOverridesFor', () => {
it('should return an empty object for undefined values', () => {
expect(getOverridesFor(undefined, 'settings')).toEqual({});
// @ts-expect-error
expect(getOverridesFor({}, 'settings')).toEqual({});
// @ts-expect-error
expect(getOverridesFor({ otherOverride: {} }, 'settings')).toEqual({});
});
it('should return only the component specific overrides', () => {
expect(
getOverridesFor({ otherOverride: { a: 15 }, settings: { b: 10 } }, 'settings')
).toEqual({ b: 10 });
});
it('should swap any "ignore" value into undefined value', () => {
expect(
getOverridesFor({ otherOverride: { a: 15 }, settings: { b: 10, c: 'ignore' } }, 'settings')
).toEqual({ b: 10, c: undefined });
});
});
});

View file

@ -5,7 +5,6 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { KibanaExecutionContext } from '@kbn/core-execution-context-common';
export const extractContainerType = (context?: KibanaExecutionContext): string | undefined => {
@ -33,3 +32,30 @@ export const extractVisualizationType = (context?: KibanaExecutionContext): stri
return recursiveGet(context)?.type;
}
};
/**
* Get an override specification and returns a props object to use directly with the Component
* @param overrides Overrides object
* @param componentName name of the Component to look for (i.e. "settings", "axisX")
* @returns an props object to use directly with the component
*/
export function getOverridesFor<
// Component props
P extends Record<string, unknown>,
// Overrides
O extends Record<string, P>,
// Overrides Component names
K extends keyof O
>(overrides: O | undefined, componentName: K) {
if (!overrides || !overrides[componentName]) {
return {};
}
return Object.fromEntries(
Object.entries(overrides[componentName]).map(([key, value]) => {
if (value === 'ignore') {
return [key, undefined];
}
return [key, value];
})
);
}

View file

@ -78,6 +78,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -131,6 +132,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -182,6 +184,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -233,6 +236,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -284,6 +288,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -337,6 +342,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -390,6 +396,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -441,6 +448,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -492,6 +500,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -543,6 +552,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -594,6 +604,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -645,6 +656,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -696,6 +708,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;
@ -747,6 +760,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
},
}
`;

View file

@ -11,6 +11,7 @@ import { GaugeArguments, GaugeShapes } from '..';
import { functionWrapper } from '@kbn/expressions-plugin/common/expression_functions/specs/tests/utils';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import {
EXPRESSION_GAUGE_NAME,
GaugeCentralMajorModes,
GaugeColorModes,
GaugeLabelMajorModes,
@ -110,4 +111,23 @@ describe('interpreter/functions#gauge', () => {
expect(loggedTable!).toMatchSnapshot();
});
it('should pass over overrides from variables', async () => {
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
};
const handlers = {
variables: { overrides },
getExecutionContext: jest.fn(),
} as unknown as ExecutionContext;
const result = await fn(context, args, handlers);
expect(result).toEqual({
type: 'render',
as: EXPRESSION_GAUGE_NAME,
value: expect.objectContaining({ overrides }),
});
});
});

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { GaugeExpressionFunctionDefinition } from '../types';
import { GaugeExpressionFunctionDefinition, GaugeRenderProps } from '../types';
import {
EXPRESSION_GAUGE_NAME,
GaugeCentralMajorModes,
@ -232,6 +232,7 @@ export const gaugeFunction = (): GaugeExpressionFunctionDefinition => ({
handlers.getExecutionContext?.()?.description,
},
canNavigateToLens: Boolean(handlers?.variables?.canNavigateToLens),
overrides: handlers.variables?.overrides as GaugeRenderProps['overrides'],
},
};
},

View file

@ -10,6 +10,7 @@ export const PLUGIN_ID = 'expressionGauge';
export const PLUGIN_NAME = 'expressionGauge';
export type {
AllowedGaugeOverrides,
GaugeExpressionFunctionDefinition,
GaugeExpressionProps,
FormatFactory,

View file

@ -15,6 +15,8 @@ import {
} from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { CustomPaletteState } from '@kbn/charts-plugin/common';
import type { MakeOverridesSerializable, Simplify } from '@kbn/chart-expressions-common/types';
import type { GoalProps } from '@elastic/charts';
import {
EXPRESSION_GAUGE_NAME,
GAUGE_FUNCTION_RENDERER_NAME,
@ -84,3 +86,7 @@ export interface Accessors {
metric?: string;
goal?: string;
}
export type AllowedGaugeOverrides = Partial<
Record<'gauge', Simplify<MakeOverridesSerializable<GoalProps>>>
>;

View file

@ -10,7 +10,8 @@ import type { PaletteRegistry } from '@kbn/coloring';
import type { PersistedState } from '@kbn/visualizations-plugin/public';
import type { ChartsPluginSetup } from '@kbn/charts-plugin/public';
import type { IFieldFormat, SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
import type { GaugeExpressionProps } from './expression_functions';
import type { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
import type { AllowedGaugeOverrides, GaugeExpressionProps } from './expression_functions';
export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
@ -20,4 +21,5 @@ export type GaugeRenderProps = GaugeExpressionProps & {
paletteService: PaletteRegistry;
renderComplete: () => void;
uiState: PersistedState;
overrides?: AllowedGaugeOverrides & AllowedSettingsOverrides;
};

View file

@ -21,7 +21,7 @@ import {
GaugeColorModes,
} from '../../common';
import GaugeComponent from './gauge_component';
import { Chart, Goal } from '@elastic/charts';
import { Chart, Goal, Settings } from '@elastic/charts';
jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');
@ -405,4 +405,19 @@ describe('GaugeComponent', function () {
expect(goal.prop('bands')).toEqual([0, 2, 6, 8, 10]);
});
});
describe('overrides', () => {
it('should apply overrides to the settings component', () => {
const component = shallowWithIntl(
<GaugeComponent
{...wrapperProps}
overrides={{ settings: { onBrushEnd: 'ignore', ariaUseDefaultSummary: true } }}
/>
);
const settingsComponent = component.find(Settings);
expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
});
});
});

View file

@ -12,6 +12,7 @@ import type { PaletteOutput } from '@kbn/coloring';
import { FieldFormat } from '@kbn/field-formats-plugin/common';
import type { CustomPaletteState } from '@kbn/charts-plugin/public';
import { EmptyPlaceholder } from '@kbn/charts-plugin/public';
import { getOverridesFor } from '@kbn/chart-expressions-common';
import { isVisDimension } from '@kbn/visualizations-plugin/common/utils';
import {
GaugeRenderProps,
@ -167,7 +168,16 @@ function getTicks(
}
export const GaugeComponent: FC<GaugeRenderProps> = memo(
({ data, args, uiState, formatFactory, paletteService, chartsThemeService, renderComplete }) => {
({
data,
args,
uiState,
formatFactory,
paletteService,
chartsThemeService,
renderComplete,
overrides,
}) => {
const {
shape: gaugeType,
palette,
@ -360,6 +370,7 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
ariaLabel={args.ariaLabel}
ariaUseDefaultSummary={!args.ariaLabel}
onRenderChange={onRenderChange}
{...getOverridesFor(overrides, 'settings')}
/>
<Goal
id="goal"
@ -396,6 +407,7 @@ export const GaugeComponent: FC<GaugeRenderProps> = memo(
labelMinor={labelMinor ? `${labelMinor}${minorExtraSpaces}` : ''}
{...extraTitles}
{...goalConfig}
{...getOverridesFor(overrides, 'gauge')}
/>
</Chart>
{commonLabel && <div className="gauge__label">{commonLabel}</div>}

View file

@ -102,6 +102,7 @@ Object {
],
"type": "datatable",
},
"overrides": undefined,
"syncCursor": true,
"syncTooltips": false,
},

View file

@ -10,7 +10,11 @@ import { heatmapFunction } from './heatmap_function';
import type { HeatmapArguments } from '..';
import { functionWrapper } from '@kbn/expressions-plugin/common/expression_functions/specs/tests/utils';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { EXPRESSION_HEATMAP_GRID_NAME, EXPRESSION_HEATMAP_LEGEND_NAME } from '../constants';
import {
EXPRESSION_HEATMAP_GRID_NAME,
EXPRESSION_HEATMAP_LEGEND_NAME,
EXPRESSION_HEATMAP_NAME,
} from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#heatmap', () => {
@ -80,4 +84,23 @@ describe('interpreter/functions#heatmap', () => {
expect(loggedTable!).toMatchSnapshot();
});
it('should pass over overrides from variables', async () => {
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
};
const handlers = {
variables: { overrides },
getExecutionContext: jest.fn(),
} as unknown as ExecutionContext;
const result = await fn(context, args, handlers);
expect(result).toEqual({
type: 'render',
as: EXPRESSION_HEATMAP_NAME,
value: expect.objectContaining({ overrides }),
});
});
});

View file

@ -14,7 +14,7 @@ import {
Dimension,
validateAccessor,
} from '@kbn/visualizations-plugin/common/utils';
import { HeatmapExpressionFunctionDefinition } from '../types';
import type { HeatmapExpressionFunctionDefinition, HeatmapExpressionProps } from '../types';
import {
EXPRESSION_HEATMAP_NAME,
EXPRESSION_HEATMAP_GRID_NAME,
@ -230,9 +230,10 @@ export const heatmapFunction = (): HeatmapExpressionFunctionDefinition => ({
(handlers.variables?.embeddableTitle as string) ??
handlers.getExecutionContext?.()?.description,
},
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
canNavigateToLens: Boolean(handlers?.variables?.canNavigateToLens),
syncTooltips: handlers.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers.isSyncCursorEnabled?.() ?? true,
canNavigateToLens: Boolean(handlers.variables?.canNavigateToLens),
overrides: handlers.variables?.overrides as HeatmapExpressionProps['overrides'],
},
};
},

View file

@ -14,7 +14,7 @@ import {
} from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { CustomPaletteState } from '@kbn/charts-plugin/common';
import { AllowedSettingsOverrides, CustomPaletteState } from '@kbn/charts-plugin/common';
import type { LegendSize } from '@kbn/visualizations-plugin/public';
import {
EXPRESSION_HEATMAP_NAME,
@ -95,6 +95,7 @@ export interface HeatmapExpressionProps {
syncTooltips: boolean;
syncCursor: boolean;
canNavigateToLens?: boolean;
overrides?: AllowedSettingsOverrides;
}
export interface HeatmapRender {

View file

@ -428,4 +428,19 @@ describe('HeatmapComponent', function () {
expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined();
expect(component.find(Settings).first().prop('onBrushEnd')).toBeUndefined();
});
describe('overrides', () => {
it('should apply overrides to the settings component', () => {
const component = shallowWithIntl(
<HeatmapComponent
{...wrapperProps}
overrides={{ settings: { onBrushEnd: 'ignore', ariaUseDefaultSummary: true } }}
/>
);
const settingsComponent = component.find(Settings);
expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
});
});
});

View file

@ -22,6 +22,7 @@ import {
ESFixedIntervalUnit,
ESCalendarIntervalUnit,
PartialTheme,
SettingsProps,
} from '@elastic/charts';
import type { CustomPaletteState } from '@kbn/charts-plugin/public';
import { search } from '@kbn/data-plugin/public';
@ -36,6 +37,7 @@ import {
} from '@kbn/visualizations-plugin/common/constants';
import { DatatableColumn } from '@kbn/expressions-plugin/public';
import { IconChartHeatmap } from '@kbn/chart-icons';
import { getOverridesFor } from '@kbn/chart-expressions-common';
import type { HeatmapRenderProps, FilterEvent, BrushEvent } from '../../common';
import {
applyPaletteParams,
@ -148,6 +150,7 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
syncTooltips,
syncCursor,
renderComplete,
overrides,
}) => {
const chartRef = useRef<Chart>(null);
const chartTheme = chartsThemeService.useChartsTheme();
@ -498,6 +501,11 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
};
});
const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
overrides,
'settings'
) as Partial<SettingsProps>;
const themeOverrides: PartialTheme = {
legend: {
labelOptions: {
@ -591,7 +599,13 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
legendColorPicker={uiState ? LegendColorPickerWrapper : undefined}
debugState={window._echDebugStateFlag ?? false}
tooltip={tooltip}
theme={[themeOverrides, chartTheme]}
theme={[
themeOverrides,
chartTheme,
...(Array.isArray(settingsThemeOverrides)
? settingsThemeOverrides
: [settingsThemeOverrides]),
]}
baseTheme={chartBaseTheme}
xDomain={{
min:
@ -606,6 +620,7 @@ export const HeatmapComponent: FC<HeatmapRenderProps> = memo(
onBrushEnd={interactive ? (onBrushEnd as BrushEndListener) : undefined}
ariaLabel={args.ariaLabel}
ariaUseDefaultSummary={!args.ariaLabel}
{...settingsOverrides}
/>
<Heatmap
id="heatmap"

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { metricVisFunction } from './metric_vis_function';
import type { MetricArguments } from '..';
import { functionWrapper } from '@kbn/expressions-plugin/common/expression_functions/specs/tests/utils';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { EXPRESSION_METRIC_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#metricVis', () => {
const fn = functionWrapper(metricVisFunction());
const context: Datatable = {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count', meta: { type: 'number' } }],
};
const args: MetricArguments = {
metric: 'col-0-1',
progressDirection: 'horizontal',
maxCols: 1,
inspectorTableId: 'random-id',
};
it('should pass over overrides from variables', async () => {
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
};
const handlers = {
variables: { overrides },
getExecutionContext: jest.fn(),
} as unknown as ExecutionContext;
const result = await fn(context, args, handlers);
expect(result).toEqual({
type: 'render',
as: EXPRESSION_METRIC_NAME,
value: expect.objectContaining({ overrides }),
});
});
});

View file

@ -14,7 +14,7 @@ import {
validateAccessor,
} from '@kbn/visualizations-plugin/common/utils';
import { LayoutDirection } from '@elastic/charts';
import { visType } from '../types';
import { MetricVisRenderConfig, visType } from '../types';
import { MetricVisExpressionFunctionDefinition } from '../types';
import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
@ -194,6 +194,7 @@ export const metricVisFunction = (): MetricVisExpressionFunctionDefinition => ({
breakdownBy: args.breakdownBy,
},
},
overrides: handlers.variables?.overrides as MetricVisRenderConfig['overrides'],
},
};
},

View file

@ -14,7 +14,7 @@ import {
ExpressionValueRender,
} from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension, prepareLogTable } from '@kbn/visualizations-plugin/common';
import { CustomPaletteState } from '@kbn/charts-plugin/common';
import type { AllowedSettingsOverrides, CustomPaletteState } from '@kbn/charts-plugin/common';
import { VisParams, visType } from './expression_renderers';
import { EXPRESSION_METRIC_NAME, EXPRESSION_METRIC_TRENDLINE_NAME } from '../constants';
@ -40,6 +40,7 @@ export interface MetricVisRenderConfig {
visType: typeof visType;
visData: Datatable;
visConfig: Pick<VisParams, 'metric' | 'dimensions'>;
overrides?: AllowedSettingsOverrides;
}
export type MetricVisExpressionFunctionDefinition = ExpressionFunctionDefinition<

View file

@ -1471,4 +1471,29 @@ describe('MetricVisComponent', function () {
expect(secondary).toBe('1.12K%');
});
});
describe('overrides', () => {
it('should apply overrides to the settings component', () => {
const component = shallow(
<MetricVis
config={{
metric: {
progressDirection: 'vertical',
maxCols: 5,
},
dimensions: {
metric: basePriceColumnId,
},
}}
data={table}
{...defaultProps}
overrides={{ settings: { onBrushEnd: 'ignore', ariaUseDefaultSummary: true } }}
/>
);
const settingsComponent = component.find(Settings);
expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
});
});
});

View file

@ -20,6 +20,7 @@ import {
Settings,
MetricWTrend,
MetricWNumber,
SettingsProps,
} from '@elastic/charts';
import { getColumnByAccessor, getFormatByAccessor } from '@kbn/visualizations-plugin/common/utils';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
@ -36,6 +37,8 @@ import { CUSTOM_PALETTE } from '@kbn/coloring';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { useResizeObserver, useEuiScrollBar } from '@elastic/eui';
import { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
import { getOverridesFor } from '@kbn/chart-expressions-common';
import { DEFAULT_TRENDLINE_NAME } from '../../common/constants';
import { VisParams } from '../../common';
import {
@ -177,6 +180,7 @@ export interface MetricVisComponentProps {
fireEvent: IInterpreterRenderHandlers['event'];
renderMode: RenderMode;
filterable: boolean;
overrides?: AllowedSettingsOverrides;
}
export const MetricVis = ({
@ -186,6 +190,7 @@ export const MetricVis = ({
fireEvent,
renderMode,
filterable,
overrides,
}: MetricVisComponentProps) => {
const primaryMetricColumn = getColumnByAccessor(config.dimensions.metric, data.columns)!;
const formatPrimaryMetric = getMetricFormatter(config.dimensions.metric, data.columns);
@ -331,6 +336,11 @@ export const MetricVis = ({
);
}, [grid.length, minHeight, scrollDimensions.height]);
const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
overrides,
'settings'
) as Partial<SettingsProps>;
return (
<div
ref={scrollContainerRef}
@ -357,8 +367,11 @@ export const MetricVis = ({
background: defaultColor,
barBackground: euiThemeVars.euiColorLightShade,
},
...chartTheme,
},
chartTheme,
...(Array.isArray(settingsThemeOverrides)
? settingsThemeOverrides
: [settingsThemeOverrides]),
]}
baseTheme={baseTheme}
onRenderChange={onRenderChange}
@ -383,6 +396,7 @@ export const MetricVis = ({
}
: undefined
}
{...settingsOverrides}
/>
<Metric id="metric" data={grid} />
</Chart>

View file

@ -55,7 +55,7 @@ export const getMetricVisRenderer = (
name: EXPRESSION_METRIC_NAME,
displayName: 'metric visualization',
reuseDomNode: true,
render: async (domNode, { visData, visConfig }, handlers) => {
render: async (domNode, { visData, visConfig, overrides }, handlers) => {
const { core, plugins } = deps.getStartDeps();
handlers.onDestroy(() => {
@ -103,6 +103,7 @@ export const getMetricVisRenderer = (
fireEvent={handlers.event}
renderMode={handlers.getRenderMode()}
filterable={filterable}
overrides={overrides}
/>
</div>
</KibanaThemeProvider>,

View file

@ -45,6 +45,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"overrides": undefined,
"params": Object {
"listenOnChange": true,
},

View file

@ -27,6 +27,7 @@ Object {
"type": "render",
"value": Object {
"canNavigateToLens": false,
"overrides": undefined,
"params": Object {
"listenOnChange": true,
},
@ -168,6 +169,7 @@ Object {
"type": "render",
"value": Object {
"canNavigateToLens": false,
"overrides": undefined,
"params": Object {
"listenOnChange": true,
},

View file

@ -45,6 +45,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"overrides": undefined,
"params": Object {
"listenOnChange": true,
},

View file

@ -37,6 +37,7 @@ Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"overrides": undefined,
"params": Object {
"listenOnChange": true,
},

View file

@ -16,7 +16,7 @@ import {
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { mosaicVisFunction } from './mosaic_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#mosaicVis', () => {
@ -147,4 +147,23 @@ describe('interpreter/functions#mosaicVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
it('should pass over overrides from variables', async () => {
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
};
const handlers = {
variables: { overrides },
getExecutionContext: jest.fn(),
} as unknown as ExecutionContext;
const result = await fn(context, visConfig, handlers);
expect(result).toEqual({
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: expect.objectContaining({ overrides }),
});
});
});

View file

@ -9,7 +9,11 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import {
LegendDisplay,
type PartitionChartProps,
type PartitionVisParams,
} from '../types/expression_renderers';
import { ChartTypes, MosaicVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@ -172,6 +176,7 @@ export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({
params: {
listenOnChange: true,
},
overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},

View file

@ -17,7 +17,7 @@ import {
import { ExpressionValueVisDimension, LegendSize } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { pieVisFunction } from './pie_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#pieVis', () => {
@ -144,4 +144,23 @@ describe('interpreter/functions#pieVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
it('should pass over overrides from variables', async () => {
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
};
const handlers = {
variables: { overrides },
getExecutionContext: jest.fn(),
} as unknown as ExecutionContext;
const result = await fn(context, { ...visConfig, isDonut: false }, handlers);
expect(result).toEqual({
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: expect.objectContaining({ overrides }),
});
});
});

View file

@ -9,7 +9,12 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
import { EmptySizeRatios, LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import {
EmptySizeRatios,
LegendDisplay,
type PartitionChartProps,
type PartitionVisParams,
} from '../types/expression_renderers';
import { ChartTypes, PieVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@ -199,6 +204,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
params: {
listenOnChange: true,
},
overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},

View file

@ -16,7 +16,7 @@ import {
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { treemapVisFunction } from './treemap_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#treemapVis', () => {
@ -150,4 +150,23 @@ describe('interpreter/functions#treemapVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
it('should pass over overrides from variables', async () => {
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
};
const handlers = {
variables: { overrides },
getExecutionContext: jest.fn(),
} as unknown as ExecutionContext;
const result = await fn(context, visConfig, handlers);
expect(result).toEqual({
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: expect.objectContaining({ overrides }),
});
});
});

View file

@ -9,7 +9,11 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import {
LegendDisplay,
type PartitionChartProps,
type PartitionVisParams,
} from '../types/expression_renderers';
import { ChartTypes, TreemapVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@ -178,6 +182,7 @@ export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition =>
params: {
listenOnChange: true,
},
overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},

View file

@ -16,7 +16,7 @@ import {
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { Datatable } from '@kbn/expressions-plugin/common/expression_types/specs';
import { waffleVisFunction } from './waffle_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
import { PARTITION_LABELS_VALUE, PARTITION_VIS_RENDERER_NAME } from '../constants';
import { ExecutionContext } from '@kbn/expressions-plugin/common';
describe('interpreter/functions#waffleVis', () => {
@ -121,4 +121,23 @@ describe('interpreter/functions#waffleVis', () => {
expect(loggedTable!).toMatchSnapshot();
});
it('should pass over overrides from variables', async () => {
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
};
const handlers = {
variables: { overrides },
getExecutionContext: jest.fn(),
} as unknown as ExecutionContext;
const result = await fn(context, visConfig, handlers);
expect(result).toEqual({
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: expect.objectContaining({ overrides }),
});
});
});

View file

@ -9,7 +9,11 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
import { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import {
LegendDisplay,
type PartitionChartProps,
type PartitionVisParams,
} from '../types/expression_renderers';
import { ChartTypes, WaffleVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
@ -173,6 +177,7 @@ export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({
params: {
listenOnChange: true,
},
overrides: handlers.variables?.overrides as PartitionChartProps['overrides'],
},
};
},

View file

@ -26,6 +26,7 @@ export {
} from './expression_functions';
export type {
AllowedPartitionOverrides,
ExpressionValuePartitionLabels,
PieVisExpressionFunctionDefinition,
TreemapVisExpressionFunctionDefinition,

View file

@ -6,6 +6,8 @@
* Side Public License, v 1.
*/
import type { PartitionProps } from '@elastic/charts';
import type { MakeOverridesSerializable, Simplify } from '@kbn/chart-expressions-common/types';
import {
ExpressionFunctionDefinition,
Datatable,
@ -21,13 +23,13 @@ import {
PARTITION_LABELS_FUNCTION,
} from '../constants';
import {
RenderValue,
PieVisConfig,
type PartitionChartProps,
type PieVisConfig,
LabelPositions,
ValueFormats,
TreemapVisConfig,
MosaicVisConfig,
WaffleVisConfig,
type TreemapVisConfig,
type MosaicVisConfig,
type WaffleVisConfig,
} from './expression_renderers';
export interface PartitionLabelsArguments {
@ -63,28 +65,28 @@ export type PieVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof PIE_VIS_EXPRESSION_NAME,
Datatable,
PieVisConfig,
ExpressionValueRender<RenderValue>
ExpressionValueRender<PartitionChartProps>
>;
export type TreemapVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof TREEMAP_VIS_EXPRESSION_NAME,
Datatable,
TreemapVisConfig,
ExpressionValueRender<RenderValue>
ExpressionValueRender<PartitionChartProps>
>;
export type MosaicVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof MOSAIC_VIS_EXPRESSION_NAME,
Datatable,
MosaicVisConfig,
ExpressionValueRender<RenderValue>
ExpressionValueRender<PartitionChartProps>
>;
export type WaffleVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof WAFFLE_VIS_EXPRESSION_NAME,
Datatable,
WaffleVisConfig,
ExpressionValueRender<RenderValue>
ExpressionValueRender<PartitionChartProps>
>;
export enum ChartTypes {
@ -101,3 +103,15 @@ export type PartitionLabelsExpressionFunctionDefinition = ExpressionFunctionDefi
PartitionLabelsArguments,
ExpressionValuePartitionLabels
>;
export type AllowedPartitionOverrides = Partial<
Record<
'partition',
Simplify<
Omit<
MakeOverridesSerializable<PartitionProps>,
'id' | 'data' | 'valueAccessor' | 'valueFormatter' | 'layers' | 'layout'
>
>
>
>;

View file

@ -7,12 +7,17 @@
*/
import { Position } from '@elastic/charts';
import type { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
import type { PaletteOutput } from '@kbn/coloring';
import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
import { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import type { Datatable, DatatableColumn } from '@kbn/expressions-plugin/common';
import type { SerializedFieldFormat } from '@kbn/field-formats-plugin/common';
import type { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import type { LegendSize } from '@kbn/visualizations-plugin/public';
import { ChartTypes, ExpressionValuePartitionLabels } from './expression_functions';
import {
type AllowedPartitionOverrides,
ChartTypes,
type ExpressionValuePartitionLabels,
} from './expression_functions';
export enum EmptySizeRatios {
SMALL = 0.3,
@ -107,12 +112,13 @@ export interface WaffleVisConfig extends Omit<VisCommonConfig, 'buckets'> {
showValuesInLegend: boolean;
}
export interface RenderValue {
export interface PartitionChartProps {
visData: Datatable;
visType: ChartTypes;
visConfig: PartitionVisParams;
syncColors: boolean;
canNavigateToLens?: boolean;
overrides?: AllowedPartitionOverrides & AllowedSettingsOverrides;
}
export enum LabelPositions {

View file

@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { mosaicArgTypes, treemapMosaicConfig, data } from './shared';
@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
visType: PartitionChartProps['visType'];
syncColors: PartitionChartProps['syncColors'];
} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,

View file

@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { pieDonutArgTypes, pieConfig, data } from './shared';
@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
visType: PartitionChartProps['visType'];
syncColors: PartitionChartProps['syncColors'];
} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,

View file

@ -9,12 +9,12 @@ import { Position } from '@elastic/charts';
import {
LabelPositions,
LegendDisplay,
RenderValue,
PartitionChartProps,
PartitionVisParams,
ValueFormats,
} from '../../../common/types';
export const config: RenderValue['visConfig'] = {
export const config: PartitionChartProps['visConfig'] = {
addTooltip: true,
legendDisplay: LegendDisplay.HIDE,
metricsToLabels: { percent_uptime: 'percent_uptime' },

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { RenderValue } from '../../../common/types';
import { PartitionChartProps } from '../../../common/types';
export const data: RenderValue['visData'] = {
export const data: PartitionChartProps['visData'] = {
type: 'datatable',
columns: [
{

View file

@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { treemapArgTypes, treemapMosaicConfig, data } from './shared';
@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
visType: PartitionChartProps['visType'];
syncColors: PartitionChartProps['syncColors'];
} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,

View file

@ -10,7 +10,7 @@ import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '@kbn/presentation-util-plugin/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { ChartTypes, PartitionChartProps } from '../../common/types';
import { getStartDeps } from '../__mocks__';
import { waffleArgTypes, waffleConfig, data } from './shared';
@ -22,9 +22,9 @@ const containerSize = {
const PartitionVisRenderer = () => getPartitionVisRenderer({ getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
visType: PartitionChartProps['visType'];
syncColors: PartitionChartProps['syncColors'];
} & PartitionChartProps['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,

View file

@ -280,6 +280,7 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = `
},
},
},
Object {},
]
}
tooltip={
@ -666,6 +667,7 @@ exports[`PartitionVisComponent should render correct structure for mosaic 1`] =
},
},
},
Object {},
]
}
tooltip={
@ -1115,6 +1117,7 @@ exports[`PartitionVisComponent should render correct structure for multi-metric
},
},
},
Object {},
]
}
tooltip={
@ -1564,6 +1567,7 @@ exports[`PartitionVisComponent should render correct structure for pie 1`] = `
},
},
},
Object {},
]
}
tooltip={
@ -1950,6 +1954,7 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] =
},
},
},
Object {},
]
}
tooltip={
@ -2295,6 +2300,7 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] =
},
},
},
Object {},
]
}
tooltip={

View file

@ -329,4 +329,19 @@ describe('PartitionVisComponent', function () {
"Pie chart can't render with negative values."
);
});
describe('overrides', () => {
it('should apply overrides to the settings component', () => {
const component = shallow(
<PartitionVisComponent
{...wrapperProps}
overrides={{ settings: { onBrushEnd: 'ignore', ariaUseDefaultSummary: true } }}
/>
);
const settingsComponent = component.find(Settings);
expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
});
});
});

View file

@ -18,6 +18,7 @@ import {
TooltipType,
SeriesIdentifier,
PartitionElementEvent,
SettingsProps,
} from '@elastic/charts';
import { useEuiTheme } from '@elastic/eui';
import type { PaletteRegistry } from '@kbn/coloring';
@ -34,13 +35,15 @@ import {
IInterpreterRenderHandlers,
} from '@kbn/expressions-plugin/public';
import type { FieldFormat } from '@kbn/field-formats-plugin/common';
import { getOverridesFor } from '@kbn/chart-expressions-common';
import { consolidateMetricColumns } from '../../common/utils';
import { DEFAULT_PERCENT_DECIMALS } from '../../common/constants';
import {
PartitionVisParams,
BucketColumns,
type BucketColumns,
ValueFormats,
PieContainerDimensions,
type PieContainerDimensions,
type PartitionChartProps,
type PartitionVisParams,
} from '../../common/types/expression_renderers';
import {
LegendColorPickerWrapper,
@ -66,7 +69,6 @@ import {
partitionVisContainerStyle,
partitionVisContainerWithToggleStyleFactory,
} from './partition_vis_component.styles';
import { ChartTypes } from '../../common/types';
import { filterOutConfig } from '../utils/filter_out_config';
import { ColumnCellValueActions, FilterEvent, StartDeps } from '../types';
@ -78,10 +80,11 @@ declare global {
_echDebugStateFlag?: boolean;
}
}
export interface PartitionVisComponentProps {
export type PartitionVisComponentProps = Omit<
PartitionChartProps,
'navigateToLens' | 'visConfig'
> & {
visParams: PartitionVisParams;
visData: Datatable;
visType: ChartTypes;
uiState: PersistedState;
fireEvent: IInterpreterRenderHandlers['event'];
renderComplete: IInterpreterRenderHandlers['done'];
@ -89,9 +92,8 @@ export interface PartitionVisComponentProps {
chartsThemeService: ChartsPluginSetup['theme'];
palettesRegistry: PaletteRegistry;
services: Pick<StartDeps, 'data' | 'fieldFormats'>;
syncColors: boolean;
columnCellValueActions: ColumnCellValueActions;
}
};
const PartitionVisComponent = (props: PartitionVisComponentProps) => {
const {
@ -102,6 +104,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
services,
syncColors,
interactive,
overrides,
} = props;
const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]);
const chartTheme = props.chartsThemeService.useChartsTheme();
@ -354,6 +357,11 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
return 1;
}, [visData.rows, metricColumn]);
const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
overrides,
'settings'
) as Partial<SettingsProps>;
const themeOverrides = useMemo(
() => getPartitionTheme(visType, visParams, chartTheme, containerDimensions, rescaleFactor),
[visType, visParams, chartTheme, containerDimensions, rescaleFactor]
@ -489,11 +497,16 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
},
},
},
...(Array.isArray(settingsThemeOverrides)
? settingsThemeOverrides
: [settingsThemeOverrides]),
]}
baseTheme={chartBaseTheme}
onRenderChange={onRenderChange}
ariaLabel={props.visParams.ariaLabel}
ariaUseDefaultSummary={!props.visParams.ariaLabel}
{...settingsOverrides}
/>
<Partition
id={visType}
@ -517,6 +530,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
}
layers={layers}
topGroove={!visParams.labels.show ? 0 : undefined}
{...getOverridesFor(overrides, 'partition')}
/>
</Chart>
</LegendColorPickerWrapperContext.Provider>

View file

@ -25,7 +25,7 @@ import { extractContainerType, extractVisualizationType } from '@kbn/chart-expre
import { VisTypePieDependencies } from '../plugin';
import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants';
import { CellValueAction, GetCompatibleCellValueActions } from '../types';
import { ChartTypes, PartitionVisParams, RenderValue } from '../../common/types';
import { ChartTypes, type PartitionVisParams, type PartitionChartProps } from '../../common/types';
export const strings = {
getDisplayName: () =>
@ -73,14 +73,14 @@ export const getColumnCellValueActions = async (
export const getPartitionVisRenderer: (
deps: VisTypePieDependencies
) => ExpressionRenderDefinition<RenderValue> = ({ getStartDeps }) => ({
) => ExpressionRenderDefinition<PartitionChartProps> = ({ getStartDeps }) => ({
name: PARTITION_VIS_RENDERER_NAME,
displayName: strings.getDisplayName(),
help: strings.getHelpDescription(),
reuseDomNode: true,
render: async (
domNode,
{ visConfig, visData, visType, syncColors, canNavigateToLens },
{ visConfig, visData, visType, syncColors, canNavigateToLens, overrides },
handlers
) => {
const { core, plugins } = getStartDeps();
@ -127,6 +127,7 @@ export const getPartitionVisRenderer: (
services={{ data: plugins.data, fieldFormats: plugins.fieldFormats }}
syncColors={syncColors}
columnCellValueActions={columnCellValueActions}
overrides={overrides}
/>
</div>
</KibanaThemeProvider>

View file

@ -7,7 +7,7 @@
*/
import { XY_VIS_RENDERER } from '../constants';
import { LayeredXyVisFn } from '../types';
import { LayeredXyVisFn, type XYRender } from '../types';
import { logDatatables, logDatatable } from '../utils';
import {
validateMarkSizeRatioLimits,
@ -65,6 +65,7 @@ export const layeredXyVisFn: LayeredXyVisFn['fn'] = async (data, args, handlers)
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
overrides: handlers.variables?.overrides as XYRender['value']['overrides'],
},
};
};

View file

@ -355,4 +355,53 @@ describe('xyVis', () => {
},
});
});
test('should pass over overrides from variables', async () => {
const { data, args } = sampleArgs();
const { layers, ...rest } = args;
const { layerId, layerType, table, type, ...restLayerArgs } = sampleLayer;
const overrides = {
settings: {
onBrushEnd: 'ignore',
},
axisX: {
showOverlappingTicks: true,
},
};
const context = {
...createMockExecutionContext(),
variables: {
overrides,
},
};
const result = await xyVisFunction.fn(
data,
{ ...rest, ...restLayerArgs, referenceLines: [] },
context
);
expect(result).toEqual({
type: 'render',
as: XY_VIS,
value: {
args: {
...rest,
layers: [
{
layerType,
table: data,
layerId: 'dataLayers-0',
type,
...restLayerArgs,
},
],
},
canNavigateToLens: false,
syncColors: false,
syncTooltips: false,
syncCursor: true,
overrides,
},
});
});
});

View file

@ -11,7 +11,7 @@ import type { Datatable } from '@kbn/expressions-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common/expression_functions';
import { LayerTypes, XY_VIS_RENDERER, DATA_LAYER } from '../constants';
import { appendLayerIds, getAccessors, getShowLines, normalizeTable } from '../helpers';
import { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs } from '../types';
import type { DataLayerConfigResult, XYLayerConfig, XyVisFn, XYArgs, XYRender } from '../types';
import {
hasAreaLayer,
hasBarLayer,
@ -137,6 +137,7 @@ export const xyVisFn: XyVisFn['fn'] = async (data, args, handlers) => {
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
syncTooltips: handlers?.isSyncTooltipsEnabled?.() ?? false,
syncCursor: handlers?.isSyncCursorEnabled?.() ?? true,
overrides: handlers.variables?.overrides as XYRender['value']['overrides'],
},
};
};

View file

@ -12,6 +12,7 @@ export const PLUGIN_NAME = 'expressionXy';
export { LayerTypes } from './constants';
export type {
AllowedXYOverrides,
XYArgs,
EndValue,
XYRender,

View file

@ -6,14 +6,15 @@
* Side Public License, v 1.
*/
import { HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
import { $Values } from '@kbn/utility-types';
import { type AxisProps, HorizontalAlignment, Position, VerticalAlignment } from '@elastic/charts';
import type { $Values } from '@kbn/utility-types';
import type { PaletteOutput } from '@kbn/coloring';
import { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import type { Datatable, ExpressionFunctionDefinition } from '@kbn/expressions-plugin/common';
import { LegendSize } from '@kbn/visualizations-plugin/common';
import { EventAnnotationOutput } from '@kbn/event-annotation-plugin/common';
import { ExpressionValueVisDimension } from '@kbn/visualizations-plugin/common';
import { MakeOverridesSerializable, Simplify } from '@kbn/chart-expressions-common/types';
import {
AxisExtentModes,
FillStyles,
@ -497,3 +498,11 @@ export type ExtendedAnnotationLayerFn = ExpressionFunctionDefinition<
ExtendedAnnotationLayerArgs,
ExtendedAnnotationLayerConfigResult
>;
export type AllowedXYOverrides = Partial<
Record<
'axisX' | 'axisLeft' | 'axisRight',
// id and groupId should not be overridden
Simplify<Omit<MakeOverridesSerializable<AxisProps>, 'id' | 'groupId'>>
>
>;

View file

@ -7,12 +7,13 @@
*/
import { CustomAnnotationTooltip } from '@elastic/charts';
import { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
import {
AvailableAnnotationIcon,
ManualPointEventAnnotationArgs,
} from '@kbn/event-annotation-plugin/common';
import { XY_VIS_RENDERER } from '../constants';
import { XYProps } from './expression_functions';
import type { AllowedXYOverrides, XYProps } from './expression_functions';
export interface XYChartProps {
args: XYProps;
@ -20,6 +21,7 @@ export interface XYChartProps {
syncCursor: boolean;
syncColors: boolean;
canNavigateToLens?: boolean;
overrides?: AllowedXYOverrides & AllowedSettingsOverrides;
}
export interface XYRender {

View file

@ -607,19 +607,22 @@ exports[`XYChart component it renders area 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -1607,19 +1610,22 @@ exports[`XYChart component it renders bar 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -2607,19 +2613,22 @@ exports[`XYChart component it renders horizontal bar 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -3607,19 +3616,22 @@ exports[`XYChart component it renders line 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -4607,19 +4619,22 @@ exports[`XYChart component it renders stacked area 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -5607,19 +5622,22 @@ exports[`XYChart component it renders stacked bar 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -6607,19 +6625,22 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -7637,19 +7658,22 @@ exports[`XYChart component split chart should render split chart if both, splitR
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -8875,19 +8899,22 @@ exports[`XYChart component split chart should render split chart if splitColumnA
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime
@ -10106,19 +10133,22 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
showLegend={false}
showLegendExtra={false}
theme={
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
Array [
Object {
"background": Object {
"color": undefined,
},
"barSeriesStyle": Object {},
"chartMargins": Object {},
"legend": Object {
"labelOptions": Object {
"maxLines": 0,
},
},
"markSizeRatio": undefined,
},
"markSizeRatio": undefined,
}
Object {},
]
}
/>
<XYCurrentTime

View file

@ -765,7 +765,7 @@ describe('XYChart component', () => {
<XYChart {...defaultProps} args={{ ...args, ...markSizeRatioArg }} />
);
expect(component.find(Settings).at(0).prop('theme')).toEqual(
expect.objectContaining(markSizeRatioArg)
expect.arrayContaining([expect.objectContaining(markSizeRatioArg)])
);
});
@ -3468,4 +3468,84 @@ describe('XYChart component', () => {
expect(headerFormatter).not.toBeUndefined();
});
});
describe('overrides', () => {
it('should work for settings component', () => {
const { args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
args={{
...args,
layers: [{ ...(args.layers[0] as DataLayerConfig), seriesType: 'line' }],
}}
overrides={{ settings: { onBrushEnd: 'ignore', ariaUseDefaultSummary: true } }}
/>
);
const settingsComponent = component.find(Settings);
expect(settingsComponent.prop('onBrushEnd')).toBeUndefined();
expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
});
it('should work for all axes components', () => {
const args = createArgsWithLayers();
const layer = args.layers[0] as DataLayerConfig;
const component = shallow(
<XYChart
{...defaultProps}
args={{
...args,
layers: [
{
...layer,
accessors: ['a', 'b'],
decorations: [
{
type: 'dataDecorationConfig',
forAccessor: 'a',
axisId: '1',
},
{
type: 'dataDecorationConfig',
forAccessor: 'b',
axisId: '2',
},
],
table: dataWithoutFormats,
},
],
yAxisConfigs: [
{
type: 'yAxisConfig',
id: '1',
position: 'left',
},
{
type: 'yAxisConfig',
id: '2',
position: 'right',
},
],
}}
overrides={{
settings: { onBrushEnd: 'ignore', ariaUseDefaultSummary: true },
axisX: { showOverlappingTicks: true },
axisLeft: { showOverlappingTicks: true },
axisRight: { showOverlappingTicks: true },
}}
/>
);
const axes = component.find(Axis);
expect(axes).toHaveLength(3);
if (Array.isArray(axes)) {
for (const axis of axes) {
expect(axis.prop('showOverlappingTicks').toEqual(true));
}
}
});
});
});

View file

@ -31,6 +31,7 @@ import {
Tooltip,
XYChartSeriesIdentifier,
TooltipValue,
SettingsProps,
} from '@elastic/charts';
import { partition } from 'lodash';
import { IconType } from '@elastic/eui';
@ -51,6 +52,7 @@ import {
LegendSizeToPixels,
} from '@kbn/visualizations-plugin/common/constants';
import { PersistedState } from '@kbn/visualizations-plugin/public';
import { getOverridesFor } from '@kbn/chart-expressions-common';
import type {
FilterEvent,
BrushEvent,
@ -226,6 +228,7 @@ export function XYChart({
renderComplete,
uiState,
timeFormat,
overrides,
}: XYChartRenderProps) {
const {
legend,
@ -792,6 +795,11 @@ export function XYChart({
// enable the tooltip actions only if there is at least one splitAccessor to the dataLayer
const hasTooltipActions = dataLayers.some((dataLayer) => dataLayer.splitAccessors) && interactive;
const { theme: settingsThemeOverrides = {}, ...settingsOverrides } = getOverridesFor(
overrides,
'settings'
) as Partial<SettingsProps>;
return (
<div css={chartContainerStyle}>
{showLegend !== undefined && uiState && (
@ -886,31 +894,36 @@ export function XYChart({
showLegend={showLegend}
legendPosition={legend?.isInside ? legendInsideParams : legend.position}
legendSize={LegendSizeToPixels[legend.legendSize ?? DEFAULT_LEGEND_SIZE]}
theme={{
...chartTheme,
barSeriesStyle: {
...chartTheme.barSeriesStyle,
...valueLabelsStyling,
theme={[
{
...chartTheme,
barSeriesStyle: {
...chartTheme.barSeriesStyle,
...valueLabelsStyling,
},
background: {
color: undefined, // removes background for embeddables
},
legend: {
labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 },
},
// if not title or labels are shown for axes, add some padding if required by reference line markers
chartMargins: {
...chartTheme.chartPaddings,
...computeChartMargins(
linesPaddings,
{ ...tickLabelsVisibilitySettings, x: xAxisConfig?.showLabels },
{ ...axisTitlesVisibilitySettings, x: xAxisConfig?.showTitle },
yAxesMap,
shouldRotate
),
},
markSizeRatio: args.markSizeRatio,
},
background: {
color: undefined, // removes background for embeddables
},
legend: {
labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 },
},
// if not title or labels are shown for axes, add some padding if required by reference line markers
chartMargins: {
...chartTheme.chartPaddings,
...computeChartMargins(
linesPaddings,
{ ...tickLabelsVisibilitySettings, x: xAxisConfig?.showLabels },
{ ...axisTitlesVisibilitySettings, x: xAxisConfig?.showTitle },
yAxesMap,
shouldRotate
),
},
markSizeRatio: args.markSizeRatio,
}}
...(Array.isArray(settingsThemeOverrides)
? settingsThemeOverrides
: [settingsThemeOverrides]),
]}
baseTheme={chartBaseTheme}
allowBrushingLastHistogramBin={isTimeViz}
rotation={shouldRotate ? 90 : 0}
@ -940,6 +953,7 @@ export function XYChart({
}
: undefined
}
{...settingsOverrides}
/>
<XYCurrentTime
enabled={Boolean(args.addTimeMarker && isTimeViz)}
@ -968,6 +982,7 @@ export function XYChart({
showOverlappingLabels={xAxisConfig?.showOverlappingLabels}
showDuplicatedTicks={xAxisConfig?.showDuplicates}
timeAxisLayerCount={shouldUseNewTimeAxis ? 2 : 0}
{...getOverridesFor(overrides, 'axisX')}
/>
{isSplitChart && splitTable && (
<SplitChart
@ -999,6 +1014,10 @@ export function XYChart({
domain={getYAxisDomain(axis)}
showOverlappingLabels={axis.showOverlappingLabels}
showDuplicatedTicks={axis.showDuplicates}
{...getOverridesFor(
overrides,
/left/i.test(axis.groupId) ? 'axisLeft' : 'axisRight'
)}
/>
);
})}

View file

@ -18,7 +18,7 @@ export type {
export { palette, systemPalette } from './expressions/palette';
export { paletteIds, defaultCustomColors } from './constants';
export type { ColorSchema, RawColorSchema, ColorMap } from './static';
export type { AllowedSettingsOverrides, ColorSchema, RawColorSchema, ColorMap } from './static';
export {
ColorSchemas,
vislibColorMaps,

View file

@ -18,3 +18,4 @@ export {
export { ColorMode, LabelRotation, defaultCountLabel } from './components';
export * from './styles';
export type { AllowedSettingsOverrides } from './overrides';

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './settings';

View file

@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { SettingsProps } from '@elastic/charts';
type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
// Overrides should not expose Functions, React nodes and children props
// So filter out any type which is not serializable
export type MakeOverridesSerializable<T> = {
[KeyType in keyof T]: NonNullable<T[KeyType]> extends Function
? // cannot use boolean here as it would be challenging to distinguish
// between a "native" boolean props and a disabled callback
// so use a specific keyword
'ignore'
: // be careful here to not filter out string/number types
NonNullable<T[KeyType]> extends React.ReactChildren | React.ReactElement
? never
: // make it recursive
NonNullable<T[KeyType]> extends object
? MakeOverridesSerializable<T[KeyType]>
: NonNullable<T[KeyType]>;
};
export type AllowedSettingsOverrides = Partial<
Record<
'settings',
Simplify<
MakeOverridesSerializable<
Omit<
SettingsProps,
| 'onRenderChange'
| 'onPointerUpdate'
| 'orderOrdinalBinsBy'
| 'baseTheme'
| 'legendColorPicker'
>
>
>
>
>;

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { Position, RecursivePartial, AxisStyle } from '@elastic/charts';
import { Position, type RecursivePartial, type AxisStyle } from '@elastic/charts';
export const MULTILAYER_TIME_AXIS_STYLE: RecursivePartial<AxisStyle> = {
tickLabel: {

View file

@ -36,10 +36,18 @@ import type {
RangeIndexPatternColumn,
PieVisualizationState,
MedianIndexPatternColumn,
MetricVisualizationState,
} from '@kbn/lens-plugin/public';
import type { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import { CodeEditor, HJsonLang, KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import type { StartDependencies } from './plugin';
import {
AllOverrides,
AttributesMenu,
LensAttributesByType,
OverridesMenu,
PanelMenu,
} from './controls';
type RequiredType = 'date' | 'string' | 'number';
type FieldsMap = Record<RequiredType, string>;
@ -78,6 +86,13 @@ function getColumnFor(type: RequiredType, fieldName: string, isBucketed: boolean
maxBars: 'auto',
format: undefined,
parentFormat: undefined,
ranges: [
{
from: 0,
to: 1000,
label: '',
},
],
},
} as RangeIndexPatternColumn;
}
@ -162,12 +177,12 @@ function getBaseAttributes(
// Generate a Lens state based on some app-specific input parameters.
// `TypedLensByValueInput` can be used for type-safety - it uses the same interfaces as Lens-internal code.
function getLensAttributes(
function getLensAttributesXY(
defaultIndexPattern: DataView,
fields: FieldsMap,
chartType: 'bar_stacked' | 'line' | 'area',
chartType: XYState['preferredSeriesType'],
color: string
): TypedLensByValueInput['attributes'] {
): LensAttributesByType<'lnsXY'> {
const baseAttributes = getBaseAttributes(defaultIndexPattern, fields);
const xyConfig: XYState = {
@ -203,7 +218,7 @@ function getLensAttributes(
function getLensAttributesHeatmap(
defaultIndexPattern: DataView,
fields: FieldsMap
): TypedLensByValueInput['attributes'] {
): LensAttributesByType<'lnsHeatmap'> {
const initialType = getInitialType(defaultIndexPattern);
const dataLayer = getDataLayer(initialType, fields[initialType]);
const heatmapDataLayer = {
@ -252,7 +267,7 @@ function getLensAttributesHeatmap(
function getLensAttributesDatatable(
defaultIndexPattern: DataView,
fields: FieldsMap
): TypedLensByValueInput['attributes'] {
): LensAttributesByType<'lnsDatatable'> {
const initialType = getInitialType(defaultIndexPattern);
const baseAttributes = getBaseAttributes(defaultIndexPattern, fields, initialType);
@ -274,8 +289,9 @@ function getLensAttributesDatatable(
function getLensAttributesGauge(
defaultIndexPattern: DataView,
fields: FieldsMap
): TypedLensByValueInput['attributes'] {
fields: FieldsMap,
shape: GaugeVisualizationState['shape'] = 'horizontalBullet'
): LensAttributesByType<'lnsGauge'> {
const dataLayer = getDataLayer('number', fields.number, false);
const gaugeDataLayer = {
columnOrder: ['col1'],
@ -288,7 +304,7 @@ function getLensAttributesGauge(
const gaugeConfig: GaugeVisualizationState = {
layerId: 'layer1',
layerType: 'data',
shape: 'horizontalBullet',
shape,
ticksPosition: 'auto',
labelMajorMode: 'auto',
metricAccessor: 'col1',
@ -306,7 +322,7 @@ function getLensAttributesGauge(
function getLensAttributesPartition(
defaultIndexPattern: DataView,
fields: FieldsMap
): TypedLensByValueInput['attributes'] {
): LensAttributesByType<'lnsPie'> {
const baseAttributes = getBaseAttributes(defaultIndexPattern, fields, 'number');
const pieConfig: PieVisualizationState = {
layers: [
@ -317,7 +333,7 @@ function getLensAttributesPartition(
layerType: 'data',
numberDisplay: 'percent',
categoryDisplay: 'default',
legendDisplay: 'default',
legendDisplay: 'show',
},
],
shape: 'pie',
@ -332,6 +348,30 @@ function getLensAttributesPartition(
};
}
function getLensAttributesMetric(
defaultIndexPattern: DataView,
fields: FieldsMap,
color: string
): LensAttributesByType<'lnsMetric'> {
const dataLayer = getDataLayer('string', fields.number, true);
const baseAttributes = getBaseAttributes(defaultIndexPattern, fields, 'number', dataLayer);
const metricConfig: MetricVisualizationState = {
layerId: 'layer1',
layerType: 'data',
metricAccessor: 'col2',
color,
breakdownByAccessor: 'col1',
};
return {
...baseAttributes,
visualizationType: 'lnsMetric',
state: {
...baseAttributes.state,
visualization: metricConfig,
},
};
}
function getFieldsByType(dataView: DataView) {
const aggregatableFields = dataView.fields.filter((f) => f.aggregatable);
const fields: Partial<FieldsMap> = {
@ -350,10 +390,6 @@ function getFieldsByType(dataView: DataView) {
return fields as FieldsMap;
}
function isXYChart(attributes: TypedLensByValueInput['attributes']) {
return attributes.visualizationType === 'lnsXY';
}
function checkAndParseSO(newSO: string) {
try {
return JSON.parse(newSO) as TypedLensByValueInput['attributes'];
@ -394,23 +430,29 @@ export const App = (props: {
to: 'now',
});
const initialColor = '#D6BF57';
const defaultCharts = [
{
id: 'bar_stacked',
attributes: getLensAttributes(props.defaultDataView, fields, 'bar_stacked', 'green'),
attributes: getLensAttributesXY(props.defaultDataView, fields, 'bar_stacked', initialColor),
},
{
id: 'line',
attributes: getLensAttributes(props.defaultDataView, fields, 'line', 'green'),
attributes: getLensAttributesXY(props.defaultDataView, fields, 'line', initialColor),
},
{
id: 'area',
attributes: getLensAttributes(props.defaultDataView, fields, 'area', 'green'),
attributes: getLensAttributesXY(props.defaultDataView, fields, 'area', initialColor),
},
{ id: 'pie', attributes: getLensAttributesPartition(props.defaultDataView, fields) },
{ id: 'table', attributes: getLensAttributesDatatable(props.defaultDataView, fields) },
{ id: 'heatmap', attributes: getLensAttributesHeatmap(props.defaultDataView, fields) },
{ id: 'gauge', attributes: getLensAttributesGauge(props.defaultDataView, fields) },
{
id: 'metric',
attributes: getLensAttributesMetric(props.defaultDataView, fields, initialColor),
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
const charts = useMemo(() => [...defaultCharts, ...loadedCharts], [loadedCharts]);
@ -429,11 +471,13 @@ export const App = (props: {
const newAttributes = JSON.stringify(newChart.attributes, null, 2);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
// clear the overrides
setOverrides(undefined);
},
[charts]
);
const currentAttributes = useMemo(() => {
const currentAttributes: TypedLensByValueInput['attributes'] = useMemo(() => {
try {
return JSON.parse(currentSO.current);
} catch (e) {
@ -442,10 +486,11 @@ export const App = (props: {
}, [currentValid, currentSO]);
const isDisabled = !currentAttributes;
const isColorDisabled = isDisabled || !isXYChart(currentAttributes);
useDebounce(() => setErrorDebounced(hasParsingError), 500, [hasParsingError]);
const [overrides, setOverrides] = useState<AllOverrides | undefined>();
return (
<KibanaContextProvider services={{ uiSettings: props.core.uiSettings }}>
<EuiPageTemplate fullHeight template="empty">
@ -475,29 +520,28 @@ export const App = (props: {
<EuiSpacer />
<EuiFlexGroup wrap>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="lns-example-change-color"
onClick={() => {
const newColor = `rgb(${[1, 2, 3].map(() =>
Math.floor(Math.random() * 256)
)})`;
const newAttributes = JSON.stringify(
getLensAttributes(
props.defaultDataView,
fields,
currentAttributes.state.visualization.preferredSeriesType,
newColor
),
null,
2
);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
}}
isDisabled={isColorDisabled}
>
Change color
</EuiButton>
<AttributesMenu
currentSO={currentSO}
currentAttributes={currentAttributes}
saveValidSO={saveValidSO}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<OverridesMenu
currentAttributes={currentAttributes}
overrides={overrides}
setOverrides={setOverrides}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<PanelMenu
enableTriggers={enableTriggers}
toggleTriggers={toggleTriggers}
enableDefaultAction={enableDefaultAction}
setEnableDefaultAction={setEnableDefaultAction}
enableExtraAction={enableExtraAction}
setEnableExtraAction={setEnableExtraAction}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
@ -552,43 +596,7 @@ export const App = (props: {
);
}}
>
Edit in Lens (new tab)
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
aria-label="Enable triggers"
data-test-subj="lns-example-triggers"
isDisabled={isDisabled}
onClick={() => {
toggleTriggers((prevState) => !prevState);
}}
>
{enableTriggers ? 'Disable triggers' : 'Enable triggers'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
aria-label="Enable extra action"
data-test-subj="lns-example-extra-action"
isDisabled={isDisabled}
onClick={() => {
setEnableExtraAction((prevState) => !prevState);
}}
>
{enableExtraAction ? 'Disable extra action' : 'Enable extra action'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
aria-label="Enable default actions"
data-test-subj="lns-example-default-action"
isDisabled={isDisabled}
onClick={() => {
setEnableDefaultAction((prevState) => !prevState);
}}
>
{enableDefaultAction ? 'Disable default action' : 'Enable default action'}
Open in Lens (new tab)
</EuiButton>
</EuiFlexItem>
<EuiFlexItem>
@ -602,6 +610,7 @@ export const App = (props: {
style={{ height: 500 }}
timeRange={time}
attributes={currentAttributes}
overrides={overrides}
onLoad={(val) => {
setIsLoading(val);
}}

View file

@ -0,0 +1,597 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useState } from 'react';
import { isEqual } from 'lodash';
import {
EuiButton,
EuiText,
EuiSpacer,
EuiColorPicker,
EuiFormRow,
EuiPopover,
useColorPickerState,
EuiSwitch,
EuiNotificationBadge,
EuiCodeBlock,
EuiIcon,
EuiToolTip,
EuiPopoverTitle,
} from '@elastic/eui';
import type { TypedLensByValueInput } from '@kbn/lens-plugin/public';
export type LensAttributesByType<VizType> = Extract<
TypedLensByValueInput['attributes'],
{ visualizationType: VizType }
>;
function isXYChart(
attributes: TypedLensByValueInput['attributes']
): attributes is LensAttributesByType<'lnsXY'> {
return attributes.visualizationType === 'lnsXY';
}
function isPieChart(
attributes: TypedLensByValueInput['attributes']
): attributes is LensAttributesByType<'lnsPie'> {
return attributes.visualizationType === 'lnsPie';
}
function isHeatmapChart(
attributes: TypedLensByValueInput['attributes']
): attributes is LensAttributesByType<'lnsHeatmap'> {
return attributes.visualizationType === 'lnsHeatmap';
}
function isDatatable(
attributes: TypedLensByValueInput['attributes']
): attributes is LensAttributesByType<'lnsDatatable'> {
return attributes.visualizationType === 'lnsDatatable';
}
function isGaugeChart(
attributes: TypedLensByValueInput['attributes']
): attributes is LensAttributesByType<'lnsGauge'> {
return attributes.visualizationType === 'lnsGauge';
}
function isMetricChart(
attributes: TypedLensByValueInput['attributes']
): attributes is LensAttributesByType<'lnsMetric'> {
return attributes.visualizationType === 'lnsMetric';
}
function isSupportedChart(attributes: TypedLensByValueInput['attributes']) {
return (
isXYChart(attributes) ||
isPieChart(attributes) ||
isHeatmapChart(attributes) ||
isGaugeChart(attributes) ||
isMetricChart(attributes)
);
}
function mergeOverrides(
currentOverrides: AllOverrides | undefined,
newOverrides: AllOverrides | undefined,
defaultOverrides: AllOverrides
): AllOverrides | undefined {
if (currentOverrides == null || isEqual(currentOverrides, defaultOverrides)) {
return newOverrides;
}
if (newOverrides == null) {
return Object.fromEntries(
Object.entries(currentOverrides)
.map(([key, value]) => {
if (!(key in defaultOverrides)) {
return [key, value];
}
// @ts-expect-error
if (isEqual(currentOverrides[key], defaultOverrides[key])) {
return [];
}
const newObject: Partial<AllOverrides[keyof AllOverrides]> = {};
// @ts-expect-error
for (const [innerKey, innerValue] of Object.entries(currentOverrides[key])) {
// @ts-expect-error
if (!(innerKey in defaultOverrides[key])) {
// @ts-expect-error
newObject[innerKey] = innerValue;
}
}
return [key, newObject];
})
.filter((arr) => arr.length)
);
}
return {
...currentOverrides,
...newOverrides,
};
}
export function OverrideSwitch({
rowLabel,
controlLabel,
value,
override,
setOverrideValue,
helpText,
}: {
rowLabel: string;
controlLabel: string;
helpText?: string;
value: AllOverrides | undefined;
override: AllOverrides;
setOverrideValue: (v: AllOverrides | undefined) => void;
}) {
// check if value contains an object with the same structure as the default override
const rootKey = Object.keys(override)[0] as keyof AllOverrides;
const overridePath = [
rootKey,
Object.keys(override[rootKey] || {})[0] as keyof AllOverrides[keyof AllOverrides],
];
const hasOverrideEnabled = Boolean(
value && overridePath[0] in value && overridePath[1] in value[overridePath[0]]!
);
return (
<EuiFormRow
label={
<EuiToolTip
content={<CodeExample propName="overrides" code={JSON.stringify(override, null, 2)} />}
position="right"
>
<span>
{rowLabel} <EuiIcon type="questionInCircle" color="subdued" />
</span>
</EuiToolTip>
}
helpText={helpText}
display="columnCompressedSwitch"
hasChildLabel={false}
>
<EuiSwitch
label={controlLabel}
name="switch"
checked={hasOverrideEnabled}
onChange={() => {
const finalOverrides = mergeOverrides(
value,
hasOverrideEnabled ? undefined : override,
override
);
setOverrideValue(finalOverrides);
}}
compressed
/>
</EuiFormRow>
);
}
function CodeExample({ propName, code }: { propName: string; code: string }) {
return (
<EuiCodeBlock language="jsx" paddingSize="none">
{`
<LensEmbeddable ${propName}={${code}} />
`}
</EuiCodeBlock>
);
}
export function AttributesMenu({
currentAttributes,
currentSO,
saveValidSO,
}: {
currentAttributes: TypedLensByValueInput['attributes'];
currentSO: React.MutableRefObject<string>;
saveValidSO: (attr: string) => void;
}) {
const [attributesPopoverOpen, setAttributesPopoverOpen] = useState(false);
const [color, setColor, errors] = useColorPickerState('#D6BF57');
return (
<EuiPopover
button={
<EuiButton
data-test-subj="lns-example-change-attributes"
onClick={() => setAttributesPopoverOpen(!attributesPopoverOpen)}
iconType="arrowDown"
iconSide="right"
color="primary"
isDisabled={!isSupportedChart(currentAttributes)}
>
Lens Attributes
</EuiButton>
}
isOpen={attributesPopoverOpen}
closePopover={() => setAttributesPopoverOpen(false)}
>
<div style={{ width: 300 }}>
{isXYChart(currentAttributes) ? (
<EuiFormRow label="Pick color" display="columnCompressed">
<EuiColorPicker
onChange={(newColor, output) => {
setColor(newColor, output);
// for sake of semplicity of this example change it locally and then shallow copy it
const dataLayer = currentAttributes.state.visualization.layers[0];
if ('yConfig' in dataLayer && dataLayer.yConfig) {
dataLayer.yConfig[0].color = newColor;
// this will make a string copy of it
const newAttributes = JSON.stringify(currentAttributes, null, 2);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
}
}}
color={color}
isInvalid={!!errors}
/>
</EuiFormRow>
) : null}
{isMetricChart(currentAttributes) ? (
<EuiFormRow label="Pick color" display="columnCompressed">
<EuiColorPicker
onChange={(newColor, output) => {
setColor(newColor, output);
// for sake of semplicity of this example change it locally and then shallow copy it
currentAttributes.state.visualization.color = newColor;
// this will make a string copy of it
const newAttributes = JSON.stringify(currentAttributes, null, 2);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
}}
color={color}
isInvalid={!!errors}
/>
</EuiFormRow>
) : null}
{isPieChart(currentAttributes) ? (
<EuiFormRow label="Show values" display="columnCompressedSwitch">
<EuiSwitch
label="As percentage"
name="switch"
checked={currentAttributes.state.visualization.layers[0].numberDisplay === 'percent'}
onChange={() => {
currentAttributes.state.visualization.layers[0].numberDisplay =
currentAttributes.state.visualization.layers[0].numberDisplay === 'percent'
? 'value'
: 'percent';
// this will make a string copy of it
const newAttributes = JSON.stringify(currentAttributes, null, 2);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
}}
compressed
/>
</EuiFormRow>
) : null}
{isHeatmapChart(currentAttributes) ? (
<EuiFormRow label="Show values" display="columnCompressedSwitch">
<EuiSwitch
label="As percentage"
name="switch"
checked={Boolean(currentAttributes.state.visualization.percentageMode)}
onChange={() => {
currentAttributes.state.visualization.percentageMode =
!currentAttributes.state.visualization.percentageMode;
// this will make a string copy of it
const newAttributes = JSON.stringify(currentAttributes, null, 2);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
}}
compressed
/>
</EuiFormRow>
) : null}
{isGaugeChart(currentAttributes) ? (
<EuiFormRow label="Ticks visibility" display="columnCompressedSwitch">
<EuiSwitch
label="Show ticks"
name="switch"
checked={Boolean(currentAttributes.state.visualization.ticksPosition !== 'hidden')}
onChange={() => {
currentAttributes.state.visualization.ticksPosition =
currentAttributes.state.visualization.ticksPosition === 'hidden'
? 'auto'
: 'hidden';
// this will make a string copy of it
const newAttributes = JSON.stringify(currentAttributes, null, 2);
currentSO.current = newAttributes;
saveValidSO(newAttributes);
}}
compressed
/>
</EuiFormRow>
) : null}
</div>
</EuiPopover>
);
}
type XYOverride = Record<'axisX' | 'axisLeft' | 'axisRight', { hide: boolean }>;
type PieOverride = Record<'partition', { fillOutside: boolean }>;
type GaugeOverride = Record<'gauge', { subtype: 'goal'; angleStart: number; angleEnd: number }>;
type SettingsOverride = Record<
'settings',
| { onBrushEnd: 'ignore' }
| {
theme: {
heatmap: { xAxisLabel: { visible: boolean }; yAxisLabel: { visible: boolean } };
};
}
| {
theme: {
metric: { border: string };
};
}
>;
export type AllOverrides = Partial<XYOverride & PieOverride & SettingsOverride & GaugeOverride>;
export function OverridesMenu({
currentAttributes,
overrides,
setOverrides,
}: {
currentAttributes: TypedLensByValueInput['attributes'];
overrides: AllOverrides | undefined;
setOverrides: (overrides: AllOverrides | undefined) => void;
}) {
const [overridesPopoverOpen, setOverridesPopoverOpen] = useState(false);
const hasOverridesEnabled = Boolean(overrides) && !isDatatable(currentAttributes);
return (
<EuiPopover
button={
<EuiButton
data-test-subj="lns-example-change-overrides"
onClick={() => setOverridesPopoverOpen(!overridesPopoverOpen)}
iconType="arrowDown"
iconSide="right"
isDisabled={!isSupportedChart(currentAttributes)}
>
Overrides{' '}
<EuiNotificationBadge color={hasOverridesEnabled ? 'accent' : 'subdued'}>
{hasOverridesEnabled ? 'ON' : 'OFF'}
</EuiNotificationBadge>
</EuiButton>
}
isOpen={overridesPopoverOpen}
closePopover={() => setOverridesPopoverOpen(false)}
>
<div style={{ width: 400 }}>
<EuiPopoverTitle>Overrides</EuiPopoverTitle>
<EuiText size="s">
<p>
Overrides are local to the Embeddable and forgotten when the visualization is open in
the Editor. They should be used carefully for specific tweaks within the integration.
</p>
<p>
There are mainly 2 use cases for overrides:
<ul>
<li>Specific styling/tuning feature missing in Lens</li>
<li>Disable specific chart behaviour</li>
</ul>
</p>
<p>Here&#39;s some examples:</p>
</EuiText>
<EuiSpacer />
{isXYChart(currentAttributes) ? (
<OverrideSwitch
override={{
settings: { onBrushEnd: 'ignore' },
}}
value={overrides}
setOverrideValue={setOverrides}
rowLabel="Brush override"
controlLabel="Disable brush action"
helpText={`This override disables the brushing locally, via the special "ignore" value.`}
/>
) : null}
{isHeatmapChart(currentAttributes) ? (
<OverrideSwitch
override={{
settings: {
theme: {
heatmap: { xAxisLabel: { visible: false }, yAxisLabel: { visible: false } },
},
},
}}
value={overrides}
setOverrideValue={setOverrides}
rowLabel="Axis override"
controlLabel="Hide all axes"
helpText={`Heatmap axis override is set via the settings component.`}
/>
) : null}
{isPieChart(currentAttributes) ? (
<OverrideSwitch
override={{
partition: { fillOutside: true },
}}
value={overrides}
setOverrideValue={setOverrides}
rowLabel="Partition override"
controlLabel="Label outsides"
/>
) : null}
{isXYChart(currentAttributes) ? (
<OverrideSwitch
override={{
axisX: { hide: true },
axisLeft: { hide: true },
axisRight: { hide: true },
}}
value={overrides}
setOverrideValue={setOverrides}
rowLabel="Axis override"
controlLabel="Hide all axes"
/>
) : null}
{isGaugeChart(currentAttributes) ? (
<OverrideSwitch
override={{
gauge: {
subtype: 'goal',
angleStart: Math.PI + (Math.PI - (2 * Math.PI) / 2.5) / 2,
angleEnd: -(Math.PI - (2 * Math.PI) / 2.5) / 2,
},
}}
value={overrides}
setOverrideValue={setOverrides}
rowLabel="Shape override"
controlLabel="Enable Arc shape"
helpText="Note that this is used only for example purposes, the arc configuration has some conflicts with some Lens attributes."
/>
) : null}
{isMetricChart(currentAttributes) ? (
<OverrideSwitch
override={{
settings: {
theme: {
metric: {
border: '#D65757',
},
},
},
}}
value={overrides}
setOverrideValue={setOverrides}
rowLabel="Metric override"
controlLabel="Enable border color"
/>
) : null}
</div>
</EuiPopover>
);
}
export function PanelMenu({
enableTriggers,
toggleTriggers,
enableDefaultAction,
setEnableDefaultAction,
enableExtraAction,
setEnableExtraAction,
}: {
enableTriggers: boolean;
enableDefaultAction: boolean;
enableExtraAction: boolean;
toggleTriggers: (v: boolean) => void;
setEnableDefaultAction: (v: boolean) => void;
setEnableExtraAction: (v: boolean) => void;
}) {
const [panelPopoverOpen, setPanelPopoverOpen] = useState(false);
return (
<EuiPopover
button={
<EuiButton
data-test-subj="lns-example-change-overrides"
onClick={() => setPanelPopoverOpen(!panelPopoverOpen)}
iconType="arrowDown"
iconSide="right"
>
Embeddable settings
</EuiButton>
}
isOpen={panelPopoverOpen}
closePopover={() => setPanelPopoverOpen(false)}
>
<div style={{ width: 400 }}>
<EuiPopoverTitle>Embeddable settings</EuiPopoverTitle>
<EuiText size="s">
<p>
It is possible to control and customize how the Embeddables is shown, disabling the
interactivity of the chart or filtering out default actions.
</p>
</EuiText>
<EuiSpacer />
<EuiFormRow
label="Enable triggers"
display="columnCompressedSwitch"
helpText="This setting controls the interactivity of the chart: when disabled the chart won't bubble any event on user action."
>
<EuiSwitch
showLabel={false}
label="Enable triggers"
name="switch"
checked={enableTriggers}
onChange={() => {
toggleTriggers(!enableTriggers);
}}
compressed
/>
</EuiFormRow>
<EuiFormRow
label="Enable default action"
display="columnCompressedSwitch"
helpText="When disabled the default panel actions (i.e. CSV download)"
>
<EuiSwitch
showLabel={false}
label="Enable default action"
name="switch"
checked={enableDefaultAction}
onChange={() => {
setEnableDefaultAction(!enableDefaultAction);
}}
compressed
/>
</EuiFormRow>
<EuiSpacer />
<p>It is also possible to pass custom actions to the panel:</p>
<EuiSpacer />
<EuiFormRow
label={
<EuiToolTip
display="block"
content={
<CodeExample
propName="extraActions"
code={`[
{
id: 'testAction',
type: 'link',
getIconType: () => 'save',
async isCompatible(
context: ActionExecutionContext<object>
): Promise<boolean> {
return true;
},
execute: async (context: ActionExecutionContext<object>) => {
alert('I am an extra action');
return;
},
getDisplayName: () =>
'Extra action',
}
]`}
/>
}
position="right"
>
<span>
Show custom action <EuiIcon type="questionInCircle" color="subdued" />
</span>
</EuiToolTip>
}
display="columnCompressedSwitch"
helpText="Pass a consumer defined action to show in the panel context menu."
>
<EuiSwitch
showLabel={false}
label="Show custom action"
name="switch"
checked={enableExtraAction}
onChange={() => {
setEnableExtraAction(!enableExtraAction);
}}
compressed
/>
</EuiFormRow>
</div>
</EuiPopover>
);
}

View file

@ -17,6 +17,10 @@ import { layerTypes } from './layer_types';
import { CollapseFunction } from './expressions';
export type { OriginalColumn } from './expressions/map_to_columns';
export type { AllowedPartitionOverrides } from '@kbn/expression-partition-vis-plugin/common';
export type { AllowedSettingsOverrides } from '@kbn/charts-plugin/common';
export type { AllowedGaugeOverrides } from '@kbn/expression-gauge-plugin/common';
export type { AllowedXYOverrides } from '@kbn/expression-xy-plugin/common';
export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;

View file

@ -1677,4 +1677,69 @@ describe('embeddable', () => {
expect(test.initializeSavedVis).toHaveBeenCalledTimes(2);
expect(test.expressionRenderer).toHaveBeenCalledTimes(2);
});
it('should pass over the overrides as variables', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
data: dataMock,
expressionRenderer,
coreStart: {} as CoreStart,
basePath,
dataViews: {} as DataViewsContract,
capabilities: {
canSaveDashboards: true,
canSaveVisualizations: true,
discover: {},
navLinks: {},
},
inspector: inspectorPluginMock.createStartContract(),
getTrigger,
theme: themeServiceMock.createStartContract(),
visualizationMap: defaultVisualizationMap,
datasourceMap: defaultDatasourceMap,
injectFilterReferences: jest.fn(mockInjectFilterReferences),
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
indexPatterns: {},
indexPatternRefs: [],
}),
uiSettings: { get: () => undefined } as unknown as IUiSettingsClient,
},
{
timeRange: {
from: 'now-15m',
to: 'now',
},
overrides: {
settings: {
onBrushEnd: 'ignore',
},
},
} as LensEmbeddableInput
);
embeddable.render(mountpoint);
// wait one tick to give embeddable time to initialize
await new Promise((resolve) => setTimeout(resolve, 0));
expect(expressionRenderer).toHaveBeenCalledTimes(1);
expect(expressionRenderer.mock.calls[0][0]!.variables).toEqual(
expect.objectContaining({
overrides: {
settings: {
onBrushEnd: 'ignore',
},
},
})
);
});
});

View file

@ -102,6 +102,12 @@ import {
UserMessagesDisplayLocationId,
} from '../types';
import type {
AllowedPartitionOverrides,
AllowedSettingsOverrides,
AllowedGaugeOverrides,
AllowedXYOverrides,
} from '../../common/types';
import { getEditPath, DOC_TYPE } from '../../common/constants';
import { LensAttributeService } from '../lens_attribute_service';
import type { TableInspectorAdapter } from '../editor_frame_service/types';
@ -150,6 +156,18 @@ interface LensBaseEmbeddableInput extends EmbeddableInput {
export type LensByValueInput = {
attributes: LensSavedObjectAttributes;
/**
* Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline.
* Each visualization type offers various type of overrides, per component (i.e. 'setting', 'axisX', 'partition', etc...)
*
* While it is not possible to pass function/callback/handlers to the renderer, it is possible to overwrite
* the current behaviour by passing the "ignore" string to the override prop (i.e. onBrushEnd: "ignore" to stop brushing)
*/
overrides?:
| AllowedSettingsOverrides
| AllowedXYOverrides
| AllowedPartitionOverrides
| AllowedGaugeOverrides;
} & LensBaseEmbeddableInput;
export type LensByReferenceInput = SavedObjectEmbeddableInput & LensBaseEmbeddableInput;
@ -469,8 +487,18 @@ export class Embeddable
const attributesOrSavedObjectId$ = input$.pipe(
distinctUntilChanged((a, b) =>
fastIsEqual(
['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId],
['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId]
[
'attributes' in a && a.attributes,
'savedObjectId' in a && a.savedObjectId,
'overrides' in a && a.overrides,
'disableTriggers' in a && a.disableTriggers,
],
[
'attributes' in b && b.attributes,
'savedObjectId' in b && b.savedObjectId,
'overrides' in b && b.overrides,
'disableTriggers' in b && b.disableTriggers,
]
)
),
skip(1),
@ -875,6 +903,7 @@ export class Embeddable
variables={{
embeddableTitle: this.getTitle(),
...(input.palette ? { theme: { palette: input.palette } } : {}),
...('overrides' in input ? { overrides: input.overrides } : {}),
}}
searchSessionId={this.getInput().searchSessionId}
handleEvent={this.handleEvent}

View file

@ -24,9 +24,16 @@ import type { LensByReferenceInput, LensByValueInput } from './embeddable';
import type { Document } from '../persistence';
import type { FormBasedPersistedState } from '../datasources/form_based/types';
import type { XYState } from '../visualizations/xy/types';
import type { PieVisualizationState, LegacyMetricState } from '../../common/types';
import type {
PieVisualizationState,
LegacyMetricState,
AllowedGaugeOverrides,
AllowedPartitionOverrides,
AllowedSettingsOverrides,
AllowedXYOverrides,
} from '../../common/types';
import type { DatatableVisualizationState } from '../visualizations/datatable/visualization';
import type { MetricVisualizationState } from '../visualizations/metric/visualization';
import type { MetricVisualizationState } from '../visualizations/metric/types';
import type { HeatmapVisualizationState } from '../visualizations/heatmap/types';
import type { GaugeVisualizationState } from '../visualizations/gauge/constants';
@ -47,16 +54,28 @@ type LensAttributes<TVisType, TVisState> = Omit<
* Type-safe variant of by value embeddable input for Lens.
* This can be used to hardcode certain Lens chart configurations within another app.
*/
export type TypedLensByValueInput = Omit<LensByValueInput, 'attributes'> & {
export type TypedLensByValueInput = Omit<LensByValueInput, 'attributes' | 'overrides'> & {
attributes:
| LensAttributes<'lnsXY', XYState>
| LensAttributes<'lnsPie', PieVisualizationState>
| LensAttributes<'lnsHeatmap', HeatmapVisualizationState>
| LensAttributes<'lnsGauge', GaugeVisualizationState>
| LensAttributes<'lnsDatatable', DatatableVisualizationState>
| LensAttributes<'lnsLegacyMetric', LegacyMetricState>
| LensAttributes<'lnsMetric', MetricVisualizationState>
| LensAttributes<'lnsHeatmap', HeatmapVisualizationState>
| LensAttributes<'lnsGauge', GaugeVisualizationState>
| LensAttributes<string, unknown>;
/**
* Overrides can tweak the style of the final embeddable and are executed at the end of the Lens rendering pipeline.
* XY charts offer an override of the Settings ('settings') and Axis ('axisX', 'axisLeft', 'axisRight') components.
* While it is not possible to pass function/callback/handlers to the renderer, it is possible to stop them by passing the
* "ignore" string as override value (i.e. onBrushEnd: "ignore")
*/
overrides?:
| AllowedSettingsOverrides
| AllowedXYOverrides
| AllowedPartitionOverrides
| AllowedGaugeOverrides;
};
export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & {

View file

@ -44,6 +44,7 @@ export type {
export type { DatatableVisualizationState } from './visualizations/datatable/visualization';
export type { HeatmapVisualizationState } from './visualizations/heatmap/types';
export type { GaugeVisualizationState } from './visualizations/gauge/constants';
export type { MetricVisualizationState } from './visualizations/metric/types';
export type {
FormBasedPersistedState,
PersistedIndexPatternLayer,

View file

@ -6,7 +6,7 @@
*/
import type { GaugeState as GaugeStateOriginal } from '@kbn/expression-gauge-plugin/common';
import { LayerType } from '../../../common/types';
import type { LayerType } from '../../../common/types';
export const LENS_GAUGE_ID = 'lnsGauge';

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { LayoutDirection } from '@elastic/charts';
import type { PaletteOutput, CustomPaletteParams } from '@kbn/coloring';
import type { CollapseFunction } from '@kbn/visualizations-plugin/common';
import type { LayerType } from '../../../common/types';
export interface MetricVisualizationState {
layerId: string;
layerType: LayerType;
metricAccessor?: string;
secondaryMetricAccessor?: string;
maxAccessor?: string;
breakdownByAccessor?: string;
// the dimensions can optionally be single numbers
// computed by collapsing all rows
collapseFn?: CollapseFunction;
subtitle?: string;
secondaryPrefix?: string;
progressDirection?: LayoutDirection;
showBar?: boolean;
color?: string;
palette?: PaletteOutput<CustomPaletteParams>;
maxCols?: number;
trendlineLayerId?: string;
trendlineLayerType?: LayerType;
trendlineTimeAccessor?: string;
trendlineMetricAccessor?: string;
trendlineSecondaryMetricAccessor?: string;
trendlineBreakdownByAccessor?: string;
}

View file

@ -25,11 +25,13 @@ When adding visualizations to a solution page, there are multiple ways to approa
Pros:
* No need to manage searches and rendering logic on your own
* "Open in Lens" comes for free
* Simple extended visualization options - if Lens can't do it, there's also a limited set of overrides to customize the final result
Cons:
* Each panel does its own data fetching and rendering (can lead to performance problems for high number of embeddables on a single page, e.g. more than 20)
* Limited data processing options - if the Lens UI doesn't support it, it can't be used
* Limited visualization options - if Lens can't do it, it's not possible
* #### **Using custom data fetching and rendering**
In case the disadvantages of using the Lens embeddable heavily affect your use case, it sometimes makes sense to roll your own data fetching and rendering by using the underlying APIs of search service and `elastic-charts` directly. This allows a high degree of flexibility when it comes to data processing, efficiently querying data for multiple charts in a single query and adjusting small details in how charts are rendered. However, do not choose these option lightly as maintenance as well as initial development effort will most likely be much higher than by using the Lens embeddable directly. In this case, almost always an "Open in Lens" button can still be offered to the user to drill down and further explore the data by generating a Lens configuration which is similar to the displayed visualization given the possibilities of Lens. Keep in mind that for the "Open in Lens" flow, the most important property isn't perfect fidelity of the chart but retaining the mental context of the user when switching so they don't have to start over. It's also possible to mix this approach with Lens embeddables on a single page. **Note**: In this situation, please let the Visualizations team know what features you are missing / why you chose not to use Lens.
@ -182,6 +184,23 @@ The Lens embeddable is handling both data fetching and rendering - all the user
/>
```
## Overrides
The Lens embeddable offers a way to extends the current set of visualization feature provided within the Lens editor, via the `overrides` property, which enables the consumer to override some visualization configurations in the embeddable instance.
```tsx
<EmbeddableComponent
// ...
overrides={{
settings: {legendAction: 'ignore'},
axisX: {hide: true}
}}
/>
```
The each override is component-specific and it inherits the prop from its `elastic-charts` definition directly. Callback/handlers are not supported as functions, but the special value `"ignore"` can be provided in order to disable them in the embeddable rendering.
**Note**: overrides are only applied to the local embeddable instance and will disappear when the visualization is open in the Lens editor.
# Lens Development
The following sections are concerned with developing the Lens plugin itself.