[Infrastructure UI] Lens Embeddable attributes builder refactor (#161281)

relates to [#160381](https://github.com/elastic/kibana/issues/160381)
closes [#161432](https://github.com/elastic/kibana/issues/161432)

## Summary

This pull request aims to enhance the usage of Lens Embeddable by
introducing abstractions that simplify the creation of the necessary
data structures for rendering charts. The goal is to improve the DX by
providing clearer and more intuitive interfaces.

More details in the
[README.md](62a9ef70e6/x-pack/plugins/infra/public/common/visualizations/lens/README.md)

### For reviewers

- The majority of the changes are concentrated in the
`common/visualizations` directory. The `formulas/host` files have been
modified to only contain the formula itself. The formulas are now
designed to be independent of specific chart styles.

- The `use_lens_attributes` hook has been modified to receive the
essential information. Previously, it had knowledge of host formulas,
but now it accepts an abstracted chart representation and returns the
JSON object accordingly.

- Chart-specific style configurations have been moved to the chart usage
component in the `tile` and `metric_chart` components.


### How to test 

- Start a local Kibana instance
- Navigate to `Infrastructure` > `Hosts`
- Check all charts and navigate to Lens.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Carlos Crespo 2023-07-10 16:06:04 +02:00 committed by GitHub
parent 9f2a75be3b
commit 4baeafe4e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1740 additions and 864 deletions

View file

@ -20,7 +20,6 @@ import {
tx,
hostCount,
} from './lens/formulas/host';
import { LineChart, MetricChart } from './lens/visualization_types';
export const hostLensFormulas = {
cpuUsage,
@ -38,9 +37,4 @@ export const hostLensFormulas = {
tx,
};
export const visualizationTypes = {
lineChart: LineChart,
metricChart: MetricChart,
};
export const HOST_METRICS_DOC_HREF = 'https://ela.st/docs-infra-host-metrics';

View file

@ -7,16 +7,16 @@
export type {
HostsLensFormulas,
LineChartOptions,
LensChartConfig,
LensLineChartConfig,
MetricChartOptions,
HostsLensMetricChartFormulas,
HostsLensLineChartFormulas,
LensOptions,
LensAttributes,
FormulaConfig,
Chart,
LensVisualizationState,
} from './types';
export { hostLensFormulas, visualizationTypes } from './constants';
export { hostLensFormulas } from './constants';
export * from './lens/visualization_types';
export { LensAttributesBuilder } from './lens/lens_attributes_builder';

View file

@ -0,0 +1,166 @@
# Lens Attributes Builder
The Lens Attributes Builder is a utility for creating JSON objects used to render charts with Lens. It simplifies the process of configuring and building the necessary attributes for different chart types.
## Usage
### Creating a Metric Chart
To create a metric chart, use the `MetricChart` class and provide the required configuration. Here's an example:
```ts
const metricChart = new MetricChart({
layers: new MetricLayer({
data: {
label: 'Disk Read Throughput',
value: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
},
},
formulaAPI,
}),
dataView,
});
```
### Creating an XY Chart
To create an XY chart, use the `XYChart` class and provide the required configuration. Here's an example:
```ts
const xyChart = new XYChart({
layers: [new XYDataLayer({
data: [{
label: 'Normalized Load',
value: "average(system.load.1) / max(system.load.cores)",
format: {
id: 'percent',
params: {
decimals: 1,
},
},
}],
formulaAPI,
})],
dataView,
});
```
### Adding Multiple Layers to an XY Chart
An XY chart can have multiple layers. Here's an example of containing a Reference Line Layer:
```ts
const xyChart = new XYChart({
layers: [
new XYDataLayer({
data: [{
label: 'Disk Read Throughput',
value: "average(system.load.1) / max(system.load.cores)",
format: {
id: 'percent',
params: {
decimals: 1,
},
},
}],
formulaAPI,
}),
new XYReferenceLineLayer({
data: [{
value: "1",
format: {
id: 'percent',
params: {
decimals: 1,
},
},
}],
}),
],
dataView,
});
```
### Adding Multiple Data Sources in the Same Layer
In an XY chart, it's possible to define multiple data sources within the same layer.
To configure multiple data sources in an XY data layer, simply provide an array of data to the same YXDataLayer class:
```ts
const xyChart = new XYChart({
layers: new YXDataLayer({
data: [{
label: 'RX',
value: "average(host.network.ingress.bytes) * 8 / (max(metricset.period, kql='host.network.ingress.bytes: *') / 1000)",
format: {
id: 'bits',
params: {
decimals: 1,
},
},
},{
label: 'TX',
value: "(average(host.network.egresss.bytes) * 8 / (max(metricset.period, kql='host.network.egresss.bytes: *') / 1000)",
format: {
id: 'bits',
params: {
decimals: 1,
},
},
}],
formulaAPI,
}),
dataView,
});
```
### Building Lens Chart Attributes
The `LensAttributesBuilder` is responsible for creating the full JSON object that combines the attributes returned by the chart classes. Here's an example:
```ts
const builder = new LensAttributesBuilder({ visualization: xyChart });
const attributes = builder.build();
```
The `attributes` object contains the final JSON representation of the chart configuration and can be used to render the chart with Lens.
### Usage with Lens EmbeddableComponent
To display the charts rendered with the Lens Attributes Builder, it's recommended to use the Lens `EmbeddableComponent`. The `EmbeddableComponent` abstracts some of the chart styling and other details that would be challenging to handle directly with the Lens Attributes Builder.
```tsx
const builder = new LensAttributesBuilder({
visualization: new MetricChart({
layers: new MetricLayer({
data: {
label: 'Disk Read Throughput',
value: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
},
},
formulaAPI,
}),
dataView,
})
});
const lensAttributes = builder.build();
<EmbeddableComponent
attributes={lensAttributes}
viewMode={ViewMode.VIEW}
...
/>
```

View file

@ -0,0 +1,115 @@
/*
* 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 { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { i18n } from '@kbn/i18n';
import { Layer } from '../../../../../hooks/use_lens_attributes';
import { hostLensFormulas } from '../../../constants';
import { FormulaConfig } from '../../../types';
import { TOOLTIP } from './translations';
import { MetricLayerOptions } from '../../visualization_types/layers';
export interface KPIChartProps
extends Pick<TypedLensByValueInput, 'id' | 'title' | 'overrides' | 'style'> {
layers: Layer<MetricLayerOptions, FormulaConfig, 'data'>;
toolTip: string;
}
export const KPI_CHARTS: KPIChartProps[] = [
{
id: 'cpuUsage',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpuUsage.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.cpuUsage,
format: {
...hostLensFormulas.cpuUsage.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#F1D86F',
showTrendLine: true,
},
},
toolTip: TOOLTIP.cpuUsage,
},
{
id: 'normalizedLoad1m',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.normalizedLoad1m,
format: {
...hostLensFormulas.normalizedLoad1m.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#79AAD9',
showTrendLine: true,
},
},
toolTip: TOOLTIP.normalizedLoad1m,
},
{
id: 'memoryUsage',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memoryUsage.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.memoryUsage,
format: {
...hostLensFormulas.memoryUsage.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#A987D1',
showTrendLine: true,
},
},
toolTip: TOOLTIP.memoryUsage,
},
{
id: 'diskSpaceUsage',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.diskSpaceUsage,
format: {
...hostLensFormulas.diskSpaceUsage.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#F5A35C',
showTrendLine: true,
},
},
toolTip: TOOLTIP.diskSpaceUsage,
},
];

View file

@ -5,32 +5,15 @@
* 2.0.
*/
import type { LensChartConfig, LensLineChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const cpuLineChart: LensLineChartConfig = {
extraVisualizationState: {
yLeftExtent: {
mode: 'custom',
lowerBound: 0,
upperBound: 1,
export const cpuUsage: FormulaConfig = {
label: 'CPU Usage',
value: '(average(system.cpu.user.pct) + average(system.cpu.system.pct)) / max(system.cpu.cores)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
};
export const cpuUsage: LensChartConfig = {
title: 'CPU Usage',
formula: {
formula:
'(average(system.cpu.user.pct) + average(system.cpu.system.pct)) / max(system.cpu.cores)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
getFilters,
lineChartConfig: cpuLineChart,
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const diskIORead: LensChartConfig = {
title: 'Disk Read IOPS',
formula: {
formula: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')",
format: {
id: 'number',
params: {
decimals: 0,
},
export const diskIORead: FormulaConfig = {
label: 'Disk Read IOPS',
value: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')",
format: {
id: 'number',
params: {
decimals: 0,
},
},
getFilters,
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const diskReadThroughput: LensChartConfig = {
title: 'Disk Read Throughput',
formula: {
formula: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
export const diskReadThroughput: FormulaConfig = {
label: 'Disk Read Throughput',
value: "counter_rate(max(system.diskio.read.count), kql='system.diskio.read.count: *')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
},
getFilters,
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const diskSpaceAvailable: LensChartConfig = {
title: 'Disk Space Available',
formula: {
formula: 'average(system.filesystem.free)',
format: {
id: 'bytes',
params: {
decimals: 0,
},
export const diskSpaceAvailable: FormulaConfig = {
label: 'Disk Space Available',
value: 'average(system.filesystem.free)',
format: {
id: 'bytes',
params: {
decimals: 0,
},
},
getFilters,
};

View file

@ -5,30 +5,15 @@
* 2.0.
*/
import type { LensChartConfig, LensLineChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const diskSpaceUsageLineChart: LensLineChartConfig = {
extraVisualizationState: {
yLeftExtent: {
mode: 'custom',
lowerBound: 0,
upperBound: 1,
export const diskSpaceUsage: FormulaConfig = {
label: 'Disk Space Usage',
value: 'average(system.filesystem.used.pct)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
};
export const diskSpaceUsage: LensChartConfig = {
title: 'Disk Space Usage',
formula: {
formula: 'average(system.filesystem.used.pct)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
getFilters,
lineChartConfig: diskSpaceUsageLineChart,
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const diskIOWrite: LensChartConfig = {
title: 'Disk Write IOPS',
formula: {
formula: "counter_rate(max(system.diskio.write.count), kql='system.diskio.write.count: *')",
format: {
id: 'number',
params: {
decimals: 0,
},
export const diskIOWrite: FormulaConfig = {
label: 'Disk Write IOPS',
value: "counter_rate(max(system.diskio.write.count), kql='system.diskio.write.count: *')",
format: {
id: 'number',
params: {
decimals: 0,
},
},
getFilters,
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const diskWriteThroughput: LensChartConfig = {
title: 'Disk Write Throughput',
formula: {
formula: "counter_rate(max(system.diskio.write.count), kql='system.diskio.write.count: *')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
export const diskWriteThroughput: FormulaConfig = {
label: 'Disk Write Throughput',
value: "counter_rate(max(system.diskio.write.count), kql='system.diskio.write.count: *')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
},
getFilters,
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const hostCount: LensChartConfig = {
title: 'Hosts',
formula: {
formula: 'unique_count(host.name)',
format: {
id: 'number',
params: {
decimals: 0,
},
export const hostCount: FormulaConfig = {
label: 'Hosts',
value: 'unique_count(host.name)',
format: {
id: 'number',
params: {
decimals: 0,
},
},
getFilters,
};

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const memoryFree: LensChartConfig = {
title: 'Memory Free',
formula: {
formula: 'max(system.memory.total) - average(system.memory.actual.used.bytes)',
format: {
id: 'bytes',
params: {
decimals: 1,
},
export const memoryFree: FormulaConfig = {
label: 'Memory Free',
value: 'max(system.memory.total) - average(system.memory.actual.used.bytes)',
format: {
id: 'bytes',
params: {
decimals: 1,
},
},
getFilters,
};

View file

@ -5,30 +5,15 @@
* 2.0.
*/
import type { LensChartConfig, LensLineChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
const memoryLineChart: LensLineChartConfig = {
extraVisualizationState: {
yLeftExtent: {
mode: 'custom',
lowerBound: 0,
upperBound: 1,
export const memoryUsage: FormulaConfig = {
label: 'Memory Usage',
value: 'average(system.memory.actual.used.pct)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
};
export const memoryUsage: LensChartConfig = {
title: 'Memory Usage',
formula: {
formula: 'average(system.memory.actual.used.pct)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
lineChartConfig: memoryLineChart,
getFilters,
};

View file

@ -5,72 +5,15 @@
* 2.0.
*/
import type { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types';
import type { LensChartConfig, LensLineChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
const REFERENCE_LAYER = 'referenceLayer';
export const loadLineChart: LensLineChartConfig = {
extraLayers: {
[REFERENCE_LAYER]: {
linkToLayers: [],
columnOrder: ['referenceColumn'],
columns: {
referenceColumn: {
label: 'Reference',
dataType: 'number',
operationType: 'static_value',
isStaticValue: true,
isBucketed: false,
scale: 'ratio',
params: {
value: 1,
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
references: [],
customLabel: true,
} as ReferenceBasedIndexPatternColumn,
},
sampling: 1,
incompleteColumns: {},
export const normalizedLoad1m: FormulaConfig = {
label: 'Normalized Load',
value: 'average(system.load.1) / max(system.load.cores)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
extraVisualizationState: {
layers: [
{
layerId: REFERENCE_LAYER,
layerType: 'referenceLine',
accessors: ['referenceColumn'],
yConfig: [
{
forAccessor: 'referenceColumn',
axisMode: 'left',
color: '#6092c0',
},
],
},
],
},
extraReference: REFERENCE_LAYER,
};
export const normalizedLoad1m: LensChartConfig = {
title: 'Normalized Load',
formula: {
formula: 'average(system.load.1) / max(system.load.cores)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
getFilters,
lineChartConfig: loadLineChart,
};

View file

@ -5,20 +5,16 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const rx: LensChartConfig = {
title: 'Network Inbound (RX)',
formula: {
formula:
"average(host.network.ingress.bytes) * 8 / (max(metricset.period, kql='host.network.ingress.bytes: *') / 1000)",
format: {
id: 'bits',
params: {
decimals: 1,
},
export const rx: FormulaConfig = {
label: 'Network Inbound (RX)',
value:
"average(host.network.ingress.bytes) * 8 / (max(metricset.period, kql='host.network.ingress.bytes: *') / 1000)",
format: {
id: 'bits',
params: {
decimals: 1,
},
},
getFilters,
};

View file

@ -5,20 +5,16 @@
* 2.0.
*/
import type { LensChartConfig } from '../../../types';
import { getFilters } from './utils';
import type { FormulaConfig } from '../../../types';
export const tx: LensChartConfig = {
title: 'Network Outbound (TX)',
formula: {
formula:
"average(host.network.egress.bytes) * 8 / (max(metricset.period, kql='host.network.egress.bytes: *') / 1000)",
format: {
id: 'bits',
params: {
decimals: 1,
},
export const tx: FormulaConfig = {
label: 'Network Outbound (TX)',
value:
"average(host.network.egress.bytes) * 8 / (max(metricset.period, kql='host.network.egress.bytes: *') / 1000)",
format: {
id: 'bits',
params: {
decimals: 1,
},
},
getFilters,
};

View file

@ -1,21 +0,0 @@
/*
* 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 { DataViewBase } from '@kbn/es-query';
export const getFilters = ({ id }: Pick<DataViewBase, 'id'>) => [
{
meta: {
index: id,
},
query: {
exists: {
field: 'host.name',
},
},
},
];

View file

@ -1,61 +0,0 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { HostsLensMetricChartFormulas } from '../types';
import { TOOLTIP } from './translations';
export interface KPIChartProps {
title: string;
subtitle?: string;
trendLine?: boolean;
backgroundColor: string;
type: HostsLensMetricChartFormulas;
decimals?: number;
toolTip: string;
}
export const KPI_CHARTS: Array<Omit<KPIChartProps, 'loading' | 'subtitle' | 'style'>> = [
{
type: 'cpuUsage',
trendLine: true,
backgroundColor: '#F1D86F',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpuUsage.title', {
defaultMessage: 'CPU Usage',
}),
toolTip: TOOLTIP.cpuUsage,
},
{
type: 'normalizedLoad1m',
trendLine: true,
backgroundColor: '#79AAD9',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title', {
defaultMessage: 'Normalized Load',
}),
toolTip: TOOLTIP.rx,
},
{
type: 'memoryUsage',
trendLine: true,
backgroundColor: '#A987D1',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memoryUsage.title', {
defaultMessage: 'Memory Usage',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memoryUsage.tooltip', {
defaultMessage: 'Main memory usage excluding page cache.',
}),
},
{
type: 'diskSpaceUsage',
trendLine: true,
backgroundColor: '#F5A35C',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title', {
defaultMessage: 'Disk Space Usage',
}),
toolTip: TOOLTIP.tx,
},
];

View file

@ -0,0 +1,359 @@
/*
* 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 'jest-canvas-mock';
import type { DataView } from '@kbn/data-views-plugin/public';
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
import { LensAttributesBuilder } from './lens_attributes_builder';
import {
MetricChart,
MetricLayer,
XYChart,
XYDataLayer,
XYReferenceLinesLayer,
} from './visualization_types';
import type { FormulaPublicApi, GenericIndexPatternColumn } from '@kbn/lens-plugin/public';
import { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types';
import type { FormulaConfig } from '../types';
const mockDataView = {
id: 'mock-id',
title: 'mock-title',
timeFieldName: '@timestamp',
isPersisted: () => false,
getName: () => 'mock-data-view',
toSpec: () => ({}),
fields: [],
metaFields: [],
} as unknown as jest.Mocked<DataView>;
const lensPluginMockStart = lensPluginMock.createStartContract();
const getDataLayer = (formula: string): GenericIndexPatternColumn => ({
customLabel: false,
dataType: 'number',
filter: undefined,
isBucketed: false,
label: formula,
operationType: 'formula',
params: {
format: {
id: 'percent',
},
formula,
isFormulaBroken: true,
} as any,
reducedTimeRange: undefined,
references: [],
timeScale: undefined,
});
const getHistogramLayer = (interval: string, includeEmptyRows?: boolean) => ({
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: includeEmptyRows
? {
includeEmptyRows,
interval,
}
: { interval },
scale: 'interval',
sourceField: '@timestamp',
});
const REFERENCE_LINE_LAYER: ReferenceBasedIndexPatternColumn = {
customLabel: true,
dataType: 'number',
isBucketed: false,
isStaticValue: true,
label: 'Reference',
operationType: 'static_value',
params: {
format: {
id: 'percent',
},
value: '1',
} as any,
references: [],
scale: 'ratio',
};
const getFormula = (value: string): FormulaConfig => ({
value,
format: {
id: 'percent',
},
});
const AVERAGE_CPU_USER_FORMULA = 'average(system.cpu.user.pct)';
const AVERAGE_CPU_SYSTEM_FORMULA = 'average(system.cpu.system.pct)';
describe('lens_attributes_builder', () => {
let formulaAPI: FormulaPublicApi;
beforeAll(async () => {
formulaAPI = (await lensPluginMockStart.stateHelperApi()).formula;
});
describe('MetricChart', () => {
it('should build MetricChart', async () => {
const metriChart = new MetricChart({
layers: new MetricLayer({
data: getFormula(AVERAGE_CPU_USER_FORMULA),
formulaAPI,
}),
dataView: mockDataView,
});
const builder = new LensAttributesBuilder({ visualization: metriChart });
const {
state: {
datasourceStates: {
formBased: { layers },
},
visualization,
},
} = builder.build();
expect(layers).toEqual({
layer: {
columnOrder: ['metric_formula_accessor'],
columns: {
metric_formula_accessor: getDataLayer(AVERAGE_CPU_USER_FORMULA),
},
indexPatternId: 'mock-id',
},
});
expect(visualization).toEqual({
color: undefined,
layerId: 'layer',
layerType: 'data',
metricAccessor: 'metric_formula_accessor',
showBar: false,
subtitle: undefined,
});
});
it('should build MetricChart with trendline', async () => {
const metriChart = new MetricChart({
layers: new MetricLayer({
data: getFormula(AVERAGE_CPU_USER_FORMULA),
options: {
showTrendLine: true,
},
formulaAPI,
}),
dataView: mockDataView,
});
const builder = new LensAttributesBuilder({ visualization: metriChart });
const {
state: {
datasourceStates: {
formBased: { layers },
},
visualization,
},
} = builder.build();
expect(layers).toEqual({
layer: {
columnOrder: ['metric_formula_accessor'],
columns: {
metric_formula_accessor: getDataLayer(AVERAGE_CPU_USER_FORMULA),
},
indexPatternId: 'mock-id',
},
layer_trendline: {
columnOrder: ['x_date_histogram', 'metric_formula_accessor_trendline'],
columns: {
metric_formula_accessor_trendline: getDataLayer(AVERAGE_CPU_USER_FORMULA),
x_date_histogram: getHistogramLayer('auto', true),
},
indexPatternId: 'mock-id',
linkToLayers: ['layer'],
sampling: 1,
},
});
expect(visualization).toEqual({
color: undefined,
layerId: 'layer',
layerType: 'data',
metricAccessor: 'metric_formula_accessor',
showBar: false,
subtitle: undefined,
trendlineLayerId: 'layer_trendline',
trendlineLayerType: 'metricTrendline',
trendlineMetricAccessor: 'metric_formula_accessor_trendline',
trendlineTimeAccessor: 'x_date_histogram',
});
});
});
describe('XYChart', () => {
it('should build XYChart', async () => {
const xyChart = new XYChart({
layers: [
new XYDataLayer({
data: [getFormula(AVERAGE_CPU_USER_FORMULA)],
formulaAPI,
}),
],
dataView: mockDataView,
});
const builder = new LensAttributesBuilder({ visualization: xyChart });
const {
state: {
datasourceStates: {
formBased: { layers },
},
visualization,
},
} = builder.build();
expect(layers).toEqual({
layer_0: {
columnOrder: ['x_date_histogram', 'formula_accessor_0_0'],
columns: {
x_date_histogram: getHistogramLayer('auto'),
formula_accessor_0_0: getDataLayer(AVERAGE_CPU_USER_FORMULA),
},
indexPatternId: 'mock-id',
},
});
expect((visualization as any).layers).toEqual([
{
accessors: ['formula_accessor_0_0'],
layerId: 'layer_0',
layerType: 'data',
seriesType: 'line',
splitAccessor: 'aggs_breakdown',
xAccessor: 'x_date_histogram',
yConfig: [],
},
]);
});
it('should build XYChart with Reference Line layer', async () => {
const xyChart = new XYChart({
layers: [
new XYDataLayer({
data: [getFormula(AVERAGE_CPU_USER_FORMULA)],
formulaAPI,
}),
new XYReferenceLinesLayer({
data: [getFormula('1')],
}),
],
dataView: mockDataView,
});
const builder = new LensAttributesBuilder({ visualization: xyChart });
const {
state: {
datasourceStates: {
formBased: { layers },
},
visualization,
},
} = builder.build();
expect(layers).toEqual({
layer_0: {
columnOrder: ['x_date_histogram', 'formula_accessor_0_0'],
columns: {
x_date_histogram: getHistogramLayer('auto'),
formula_accessor_0_0: getDataLayer(AVERAGE_CPU_USER_FORMULA),
},
indexPatternId: 'mock-id',
},
layer_1_reference: {
columnOrder: ['formula_accessor_1_0_reference_column'],
columns: {
formula_accessor_1_0_reference_column: REFERENCE_LINE_LAYER,
},
incompleteColumns: {},
linkToLayers: [],
sampling: 1,
},
});
expect((visualization as any).layers).toEqual([
{
accessors: ['formula_accessor_0_0'],
layerId: 'layer_0',
layerType: 'data',
seriesType: 'line',
splitAccessor: 'aggs_breakdown',
xAccessor: 'x_date_histogram',
yConfig: [],
},
{
accessors: ['formula_accessor_1_0_reference_column'],
layerId: 'layer_1_reference',
layerType: 'referenceLine',
yConfig: [
{
axisMode: 'left',
color: undefined,
forAccessor: 'formula_accessor_1_0_reference_column',
},
],
},
]);
});
it('should build XYChart with multiple data columns', async () => {
const xyChart = new XYChart({
layers: [
new XYDataLayer({
data: [getFormula(AVERAGE_CPU_USER_FORMULA), getFormula(AVERAGE_CPU_SYSTEM_FORMULA)],
formulaAPI,
}),
],
dataView: mockDataView,
});
const builder = new LensAttributesBuilder({ visualization: xyChart });
const {
state: {
datasourceStates: {
formBased: { layers },
},
visualization,
},
} = builder.build();
expect(layers).toEqual({
layer_0: {
columnOrder: ['x_date_histogram', 'formula_accessor_0_0', 'formula_accessor_0_1'],
columns: {
x_date_histogram: getHistogramLayer('auto'),
formula_accessor_0_0: getDataLayer(AVERAGE_CPU_USER_FORMULA),
formula_accessor_0_1: getDataLayer(AVERAGE_CPU_SYSTEM_FORMULA),
},
indexPatternId: 'mock-id',
},
});
expect((visualization as any).layers).toEqual([
{
accessors: ['formula_accessor_0_0', 'formula_accessor_0_1'],
layerId: 'layer_0',
layerType: 'data',
seriesType: 'line',
splitAccessor: 'aggs_breakdown',
xAccessor: 'x_date_histogram',
yConfig: [],
},
]);
});
});
});

View file

@ -6,39 +6,38 @@
*/
import type {
LensAttributes,
TVisualization,
VisualizationAttributes,
LensVisualizationState,
Chart,
VisualizationAttributesBuilder,
} from '../types';
import { DataViewCache } from './data_view_cache';
import { getAdhocDataView } from './utils';
export class LensAttributesBuilder<T extends VisualizationAttributes<TVisualization>>
export class LensAttributesBuilder<T extends Chart<LensVisualizationState>>
implements VisualizationAttributesBuilder
{
private dataViewCache: DataViewCache;
constructor(private visualization: T) {
constructor(private state: { visualization: T }) {
this.dataViewCache = DataViewCache.getInstance();
}
build(): LensAttributes {
const { visualization } = this.state;
return {
title: this.visualization.getTitle(),
visualizationType: this.visualization.getVisualizationType(),
references: this.visualization.getReferences(),
title: visualization.getTitle(),
visualizationType: visualization.getVisualizationType(),
references: visualization.getReferences(),
state: {
datasourceStates: {
formBased: {
layers: this.visualization.getLayers(),
layers: visualization.getLayers(),
},
},
internalReferences: this.visualization.getReferences(),
filters: this.visualization.getFilters(),
internalReferences: visualization.getReferences(),
filters: [],
query: { language: 'kuery', query: '' },
visualization: this.visualization.getVisualizationState(),
adHocDataViews: getAdhocDataView(
this.dataViewCache.getSpec(this.visualization.getDataView())
),
visualization: visualization.getVisualizationState(),
adHocDataViews: getAdhocDataView(this.dataViewCache.getSpec(visualization.getDataView())),
},
};
}

View file

@ -4,30 +4,28 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState, useRef, useCallback, CSSProperties } from 'react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Action } from '@kbn/ui-actions-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { BrushTriggerEvent } from '@kbn/charts-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { useIntersectedOnce } from '../../../hooks/use_intersection_once';
import { TimeRange } from '@kbn/es-query';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { useIntersectedOnce } from '../../../hooks/use_intersection_once';
import { ChartLoader } from './chart_loader';
import type { LensAttributes } from '../types';
export interface LensWrapperProps {
id: string;
export interface LensWrapperProps
extends Pick<
TypedLensByValueInput,
'id' | 'overrides' | 'query' | 'filters' | 'style' | 'onBrushEnd' | 'onLoad' | 'disableTriggers'
> {
attributes: LensAttributes | null;
dateRange: TimeRange;
query?: Query;
filters: Filter[];
extraActions: Action[];
lastReloadRequestTime?: number;
style?: CSSProperties;
loading?: boolean;
hasTitle?: boolean;
onBrushEnd?: (data: BrushTriggerEvent['data']) => void;
onLoad?: () => void;
}
export const LensWrapper = React.memo(
@ -41,8 +39,10 @@ export const LensWrapper = React.memo(
style,
onBrushEnd,
lastReloadRequestTime,
overrides,
loading = false,
hasTitle = false,
disableTriggers = false,
}: LensWrapperProps) => {
const intersectionRef = useRef(null);
const [loadedOnce, setLoadedOnce] = useState(false);
@ -103,12 +103,14 @@ export const LensWrapper = React.memo(
<EmbeddableComponent
id={id}
style={style}
hidePanelTitles={!hasTitle}
attributes={state.attributes}
viewMode={ViewMode.VIEW}
timeRange={state.dateRange}
query={state.query}
filters={state.filters}
extraActions={extraActions}
overrides={overrides}
lastReloadRequestTime={state.lastReloadRequestTime}
executionContext={{
type: 'infrastructure_observability_hosts_view',
@ -116,6 +118,7 @@ export const LensWrapper = React.memo(
}}
onBrushEnd={onBrushEnd}
onLoad={onLoad}
disableTriggers={disableTriggers}
/>
)}
</ChartLoader>

View file

@ -12,8 +12,9 @@ import {
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/public';
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
export const DEFAULT_LAYER_ID = 'layer1';
export const DEFAULT_LAYER_ID = 'layer';
export const DEFAULT_AD_HOC_DATA_VIEW_ID = 'infra_lens_ad_hoc_default';
const DEFAULT_BREAKDOWN_SIZE = 10;
export const getHistogramColumn = ({
@ -37,7 +38,7 @@ export const getHistogramColumn = ({
};
};
export const getBreakdownColumn = ({
export const getTopValuesColumn = ({
columnName,
overrides,
}: {

View file

@ -5,5 +5,7 @@
* 2.0.
*/
export { LineChart } from './line_chart';
export { XYChart } from './xy_chart';
export { MetricChart } from './metric_chart';
export * from './layers';

View file

@ -0,0 +1,38 @@
/*
* 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 { FormulaPublicApi, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { FormulaConfig, ChartColumn } from '../../../../types';
export class FormulaColumn implements ChartColumn {
constructor(private formulaConfig: FormulaConfig, private formulaAPI: FormulaPublicApi) {}
getFormulaConfig(): FormulaConfig {
return this.formulaConfig;
}
getData(
id: string,
baseLayer: PersistedIndexPatternLayer,
dataView: DataView
): PersistedIndexPatternLayer {
const { value, ...rest } = this.getFormulaConfig();
const formulaLayer = this.formulaAPI.insertOrReplaceFormulaColumn(
id,
{ formula: value, ...rest },
baseLayer,
dataView
);
if (!formulaLayer) {
throw new Error('Error generating the data layer for the chart');
}
return formulaLayer;
}
}

View file

@ -0,0 +1,41 @@
/*
* 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 { PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
import type { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types';
import type { FormulaConfig, ChartColumn } from '../../../../types';
export class ReferenceLineColumn implements ChartColumn {
constructor(private formulaConfig: FormulaConfig) {}
getFormulaConfig(): FormulaConfig {
return this.formulaConfig;
}
getData(id: string, baseLayer: PersistedIndexPatternLayer): PersistedIndexPatternLayer {
const { label, ...params } = this.getFormulaConfig();
return {
linkToLayers: [],
columnOrder: [...baseLayer.columnOrder, id],
columns: {
[id]: {
label: label ?? 'Reference',
dataType: 'number',
operationType: 'static_value',
isStaticValue: true,
isBucketed: false,
scale: 'ratio',
params,
references: [],
customLabel: true,
} as ReferenceBasedIndexPatternColumn,
},
sampling: 1,
incompleteColumns: {},
};
}
}

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export { MetricLayer, type MetricLayerOptions } from './metric_layer';
export { XYDataLayer, type XYLayerOptions } from './xy_data_layer';
export { XYReferenceLinesLayer } from './xy_reference_lines_layer';
export { FormulaColumn as FormulaDataColumn } from './column/formula';
export { ReferenceLineColumn } from './column/reference_line';

View file

@ -0,0 +1,112 @@
/*
* 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 { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/common';
import type {
FormulaPublicApi,
FormBasedPersistedState,
MetricVisualizationState,
PersistedIndexPatternLayer,
} from '@kbn/lens-plugin/public';
import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types';
import { getDefaultReferences, getHistogramColumn } from '../../utils';
import { FormulaColumn } from './column/formula';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export interface MetricLayerOptions {
backgroundColor?: string;
showTitle?: boolean;
showTrendLine?: boolean;
subtitle?: string;
}
interface MetricLayerConfig {
data: FormulaConfig;
options?: MetricLayerOptions;
formulaAPI: FormulaPublicApi;
}
export class MetricLayer implements ChartLayer<MetricVisualizationState> {
private column: ChartColumn;
constructor(private layerConfig: MetricLayerConfig) {
this.column = new FormulaColumn(layerConfig.data, layerConfig.formulaAPI);
}
getLayer(
layerId: string,
accessorId: string,
dataView: DataView
): FormBasedPersistedState['layers'] {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [HISTOGRAM_COLUMN_NAME],
columns: getHistogramColumn({
columnName: HISTOGRAM_COLUMN_NAME,
overrides: {
sourceField: dataView.timeFieldName,
params: {
interval: 'auto',
includeEmptyRows: true,
},
},
}),
sampling: 1,
};
return {
[layerId]: {
...this.column.getData(
accessorId,
{
columnOrder: [],
columns: {},
},
dataView
),
},
...(this.layerConfig.options?.showTrendLine
? {
[`${layerId}_trendline`]: {
linkToLayers: [layerId],
...this.column.getData(`${accessorId}_trendline`, baseLayer, dataView),
},
}
: {}),
};
}
getReference(layerId: string, dataView: DataView): SavedObjectReference[] {
return [
...getDefaultReferences(dataView, layerId),
...getDefaultReferences(dataView, `${layerId}_trendline`),
];
}
getLayerConfig(layerId: string, accessorId: string): MetricVisualizationState {
const { subtitle, backgroundColor, showTrendLine } = this.layerConfig.options ?? {};
return {
layerId,
layerType: 'data',
metricAccessor: accessorId,
color: backgroundColor,
subtitle,
showBar: false,
...(showTrendLine
? {
trendlineLayerId: `${layerId}_trendline`,
trendlineLayerType: 'metricTrendline',
trendlineMetricAccessor: `${accessorId}_trendline`,
trendlineTimeAccessor: HISTOGRAM_COLUMN_NAME,
}
: {}),
};
}
getName(): string | undefined {
return this.column.getFormulaConfig().label;
}
}

View file

@ -0,0 +1,106 @@
/*
* 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 { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/common';
import type {
FormulaPublicApi,
FormBasedPersistedState,
PersistedIndexPatternLayer,
XYDataLayerConfig,
} from '@kbn/lens-plugin/public';
import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types';
import { getDefaultReferences, getHistogramColumn, getTopValuesColumn } from '../../utils';
import { FormulaColumn } from './column/formula';
const BREAKDOWN_COLUMN_NAME = 'aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export interface XYLayerOptions {
breakdown?: {
size: number;
sourceField: string;
};
}
interface XYLayerConfig {
data: FormulaConfig[];
options?: XYLayerOptions;
formulaAPI: FormulaPublicApi;
}
export class XYDataLayer implements ChartLayer<XYDataLayerConfig> {
private column: ChartColumn[];
constructor(private layerConfig: XYLayerConfig) {
this.column = layerConfig.data.map((p) => new FormulaColumn(p, layerConfig.formulaAPI));
}
getName(): string | undefined {
return this.column[0].getFormulaConfig().label;
}
getBaseColumnColumn(dataView: DataView, options?: XYLayerOptions) {
return {
...getHistogramColumn({
columnName: HISTOGRAM_COLUMN_NAME,
overrides: {
sourceField: dataView.timeFieldName,
},
}),
...(options?.breakdown
? {
...getTopValuesColumn({
columnName: BREAKDOWN_COLUMN_NAME,
overrides: {
sourceField: options?.breakdown.sourceField,
breakdownSize: options?.breakdown.size,
},
}),
}
: {}),
};
}
getLayer(
layerId: string,
accessorId: string,
dataView: DataView
): FormBasedPersistedState['layers'] {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...this.getBaseColumnColumn(dataView, this.layerConfig.options),
},
};
return {
[layerId]: this.column.reduce(
(acc, curr, index) => ({
...acc,
...curr.getData(`${accessorId}_${index}`, acc, dataView),
}),
baseLayer
),
};
}
getReference(layerId: string, dataView: DataView): SavedObjectReference[] {
return getDefaultReferences(dataView, layerId);
}
getLayerConfig(layerId: string, accessorId: string): XYDataLayerConfig {
return {
layerId,
seriesType: 'line',
accessors: this.column.map((_, index) => `${accessorId}_${index}`),
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
};
}
}

View file

@ -0,0 +1,65 @@
/*
* 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 { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/common';
import type {
FormBasedPersistedState,
PersistedIndexPatternLayer,
XYReferenceLineLayerConfig,
} from '@kbn/lens-plugin/public';
import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types';
import { getDefaultReferences } from '../../utils';
import { ReferenceLineColumn } from './column/reference_line';
interface XYReferenceLinesLayerConfig {
data: FormulaConfig[];
}
export class XYReferenceLinesLayer implements ChartLayer<XYReferenceLineLayerConfig> {
private column: ChartColumn[];
constructor(layerConfig: XYReferenceLinesLayerConfig) {
this.column = layerConfig.data.map((p) => new ReferenceLineColumn(p));
}
getName(): string | undefined {
return this.column[0].getFormulaConfig().label;
}
getLayer(
layerId: string,
accessorId: string,
dataView: DataView
): FormBasedPersistedState['layers'] {
const baseLayer = { columnOrder: [], columns: {} } as PersistedIndexPatternLayer;
return {
[`${layerId}_reference`]: this.column.reduce((acc, curr, index) => {
return {
...acc,
...curr.getData(`${accessorId}_${index}_reference_column`, acc, dataView),
};
}, baseLayer),
};
}
getReference(layerId: string, dataView: DataView): SavedObjectReference[] {
return getDefaultReferences(dataView, `${layerId}_reference`);
}
getLayerConfig(layerId: string, accessorId: string): XYReferenceLineLayerConfig {
return {
layerId: `${layerId}_reference`,
layerType: 'referenceLine',
accessors: this.column.map((_, index) => `${accessorId}_${index}_reference_column`),
yConfig: this.column.map((layer, index) => ({
color: layer.getFormulaConfig().color,
forAccessor: `${accessorId}_${index}_reference_column`,
axisMode: 'left',
})),
};
}
}

View file

@ -1,153 +0,0 @@
/*
* 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 {
FormBasedPersistedState,
FormulaPublicApi,
PersistedIndexPatternLayer,
XYState,
} from '@kbn/lens-plugin/public';
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/public';
import { Filter } from '@kbn/es-query';
import {
DEFAULT_LAYER_ID,
getBreakdownColumn,
getDefaultReferences,
getHistogramColumn,
} from '../utils';
import type { LensChartConfig, VisualizationAttributes, LineChartOptions } from '../../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
const ACCESSOR = 'formula_accessor';
export class LineChart implements VisualizationAttributes<XYState> {
constructor(
private chartConfig: LensChartConfig,
private dataView: DataView,
private formulaAPI: FormulaPublicApi,
private options?: LineChartOptions
) {}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers(): FormBasedPersistedState['layers'] {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn({
columnName: BREAKDOWN_COLUMN_NAME,
overrides: {
sourceField: 'host.name',
breakdownSize: this.options?.breakdownSize,
},
}),
...getHistogramColumn({
columnName: HISTOGRAM_COLUMN_NAME,
overrides: {
sourceField: this.dataView.timeFieldName,
},
}),
},
};
const dataLayer = this.formulaAPI.insertOrReplaceFormulaColumn(
ACCESSOR,
this.chartConfig.formula,
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return { [DEFAULT_LAYER_ID]: dataLayer, ...this.chartConfig.lineChartConfig?.extraLayers };
}
getVisualizationState(): XYState {
const extraVisualizationState = this.chartConfig.lineChartConfig?.extraVisualizationState;
return getXYVisualizationState({
...extraVisualizationState,
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: [ACCESSOR],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
...(extraVisualizationState?.layers ? extraVisualizationState?.layers : []),
],
});
}
getReferences(): SavedObjectReference[] {
const extraReference = this.chartConfig.lineChartConfig?.extraReference;
return [
...getDefaultReferences(this.dataView, DEFAULT_LAYER_ID),
...(extraReference ? getDefaultReferences(this.dataView, extraReference) : []),
];
}
getDataView(): DataView {
return this.dataView;
}
getTitle(): string {
return this.options?.title ?? this.chartConfig.title ?? '';
}
getFilters(): Filter[] {
return this.chartConfig.getFilters({ id: this.dataView.id ?? DEFAULT_LAYER_ID });
}
}
export const getXYVisualizationState = (
custom: Omit<Partial<XYState>, 'layers'> & { layers: XYState['layers'] }
): XYState => ({
legend: {
isVisible: false,
position: 'right',
showSingleSeries: false,
},
valueLabels: 'show',
fittingFunction: 'Zero',
curveType: 'LINEAR',
yLeftScale: 'linear',
axisTitlesVisibilitySettings: {
x: false,
yLeft: false,
yRight: true,
},
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
labelsOrientation: {
x: 0,
yLeft: 0,
yRight: 0,
},
gridlinesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
preferredSeriesType: 'line',
valuesInLegend: false,
emphasizeFitting: true,
hideEndzones: true,
...custom,
});

View file

@ -5,152 +5,39 @@
* 2.0.
*/
import {
FormBasedPersistedState,
FormulaPublicApi,
MetricVisualizationState,
PersistedIndexPatternLayer,
} from '@kbn/lens-plugin/public';
import type { FormBasedPersistedState, MetricVisualizationState } from '@kbn/lens-plugin/public';
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { Filter } from '@kbn/es-query';
import { DEFAULT_LAYER_ID, getDefaultReferences, getHistogramColumn } from '../utils';
import { DEFAULT_LAYER_ID } from '../utils';
import type {
VisualizationAttributes,
LensChartConfig,
MetricChartOptions,
Formula,
} from '../../types';
import type { Chart, ChartConfig, ChartLayer } from '../../types';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
const TRENDLINE_LAYER_ID = 'trendline_layer';
const TRENDLINE_ACCESSOR = 'metric_trendline_formula_accessor';
const ACCESSOR = 'metric_formula_accessor';
export class MetricChart implements VisualizationAttributes<MetricVisualizationState> {
constructor(
private chartConfig: LensChartConfig,
private dataView: DataView,
private formulaAPI: FormulaPublicApi,
private options?: MetricChartOptions
) {}
export class MetricChart implements Chart<MetricVisualizationState> {
constructor(private chartConfig: ChartConfig<ChartLayer<MetricVisualizationState>>) {}
getVisualizationType(): string {
return 'lnsMetric';
}
getTrendLineLayer(baseLayer: PersistedIndexPatternLayer): FormBasedPersistedState['layers'] {
const trendLineLayer = this.formulaAPI.insertOrReplaceFormulaColumn(
TRENDLINE_ACCESSOR,
this.getFormulaWithOverride(),
baseLayer,
this.dataView
);
if (!trendLineLayer) {
throw new Error('Error generating the data layer for the chart');
}
return {
[TRENDLINE_LAYER_ID]: {
linkToLayers: [DEFAULT_LAYER_ID],
...trendLineLayer,
},
};
}
getFormulaWithOverride(): Formula {
const { formula } = this.chartConfig;
const { decimals = formula.format?.params?.decimals, title = this.chartConfig.title } =
this.options ?? {};
return {
...this.chartConfig.formula,
...(formula.format && decimals
? {
format: {
...formula.format,
params: {
decimals,
},
},
}
: {}),
label: title,
};
}
getLayers(): FormBasedPersistedState['layers'] {
const { showTrendLine = true } = this.options ?? {};
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [HISTOGRAM_COLUMN_NAME],
columns: getHistogramColumn({
columnName: HISTOGRAM_COLUMN_NAME,
overrides: {
sourceField: this.dataView.timeFieldName,
params: {
interval: 'auto',
includeEmptyRows: true,
},
},
}),
sampling: 1,
};
const baseLayerDetails = this.formulaAPI.insertOrReplaceFormulaColumn(
ACCESSOR,
this.getFormulaWithOverride(),
{ columnOrder: [], columns: {} },
this.dataView
);
if (!baseLayerDetails) {
throw new Error('Error generating the data layer for the chart');
}
return {
[DEFAULT_LAYER_ID]: baseLayerDetails,
...(showTrendLine ? this.getTrendLineLayer(baseLayer) : {}),
};
return this.chartConfig.layers.getLayer(DEFAULT_LAYER_ID, ACCESSOR, this.chartConfig.dataView);
}
getVisualizationState(): MetricVisualizationState {
const { subtitle, backgroundColor, showTrendLine = true } = this.options ?? {};
return {
layerId: DEFAULT_LAYER_ID,
layerType: 'data',
metricAccessor: ACCESSOR,
color: backgroundColor,
subtitle,
showBar: false,
...(showTrendLine
? {
trendlineLayerId: TRENDLINE_LAYER_ID,
trendlineLayerType: 'metricTrendline',
trendlineMetricAccessor: TRENDLINE_ACCESSOR,
trendlineTimeAccessor: HISTOGRAM_COLUMN_NAME,
}
: {}),
};
return this.chartConfig.layers.getLayerConfig(DEFAULT_LAYER_ID, ACCESSOR);
}
getReferences(): SavedObjectReference[] {
const { showTrendLine = true } = this.options ?? {};
return [
...getDefaultReferences(this.dataView, DEFAULT_LAYER_ID),
...(showTrendLine ? getDefaultReferences(this.dataView, TRENDLINE_LAYER_ID) : []),
];
return this.chartConfig.layers.getReference(DEFAULT_LAYER_ID, this.chartConfig.dataView);
}
getDataView(): DataView {
return this.dataView;
return this.chartConfig.dataView;
}
getTitle(): string {
return this.options?.showTitle ? this.options?.title ?? this.chartConfig.title : '';
}
getFilters(): Filter[] {
return this.chartConfig.getFilters({ id: this.dataView.id ?? DEFAULT_LAYER_ID });
return this.chartConfig.title ?? '';
}
}

View file

@ -0,0 +1,99 @@
/*
* 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 { FormBasedPersistedState, XYLayerConfig, XYState } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import { DEFAULT_LAYER_ID } from '../utils';
import type { Chart, ChartConfig, ChartLayer } from '../../types';
const ACCESSOR = 'formula_accessor';
export class XYChart implements Chart<XYState> {
constructor(private chartConfig: ChartConfig<Array<ChartLayer<XYLayerConfig>>>) {}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers(): FormBasedPersistedState['layers'] {
return this.chartConfig.layers.reduce((acc, curr, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
const accessorId = `${ACCESSOR}_${index}`;
return {
...acc,
...curr.getLayer(layerId, accessorId, this.chartConfig.dataView),
};
}, {});
}
getVisualizationState(): XYState {
return getXYVisualizationState({
layers: [
...this.chartConfig.layers.map((layerItem, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
const accessorId = `${ACCESSOR}_${index}`;
return layerItem.getLayerConfig(layerId, accessorId);
}),
],
});
}
getReferences(): SavedObjectReference[] {
return this.chartConfig.layers.flatMap((p, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
return p.getReference(layerId, this.chartConfig.dataView);
});
}
getDataView(): DataView {
return this.chartConfig.dataView;
}
getTitle(): string {
return this.chartConfig.title ?? this.chartConfig.layers[0].getName() ?? '';
}
}
export const getXYVisualizationState = (
custom: Omit<Partial<XYState>, 'layers'> & { layers: XYState['layers'] }
): XYState => ({
legend: {
isVisible: false,
position: 'right',
showSingleSeries: false,
},
valueLabels: 'show',
fittingFunction: 'Zero',
curveType: 'LINEAR',
yLeftScale: 'linear',
axisTitlesVisibilitySettings: {
x: false,
yLeft: false,
yRight: true,
},
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
labelsOrientation: {
x: 0,
yLeft: 0,
yRight: 0,
},
gridlinesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
preferredSeriesType: 'line',
valuesInLegend: false,
emphasizeFitting: true,
hideEndzones: true,
...custom,
});

View file

@ -7,62 +7,75 @@
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/common';
import { DataViewBase, Filter } from '@kbn/es-query';
import {
import type {
FormBasedPersistedState,
FormulaPublicApi,
MetricVisualizationState,
PersistedIndexPatternLayer,
TypedLensByValueInput,
XYState,
XYDataLayerConfig,
FormulaPublicApi,
} from '@kbn/lens-plugin/public';
import { hostLensFormulas, visualizationTypes } from './constants';
import { hostLensFormulas } from './constants';
export type LensAttributes = TypedLensByValueInput['attributes'];
export interface LensOptions {
title: string;
}
export interface LineChartOptions extends LensOptions {
breakdownSize?: number;
}
export interface MetricChartOptions extends LensOptions {
subtitle?: string;
showTitle?: boolean;
showTrendLine?: boolean;
backgroundColor?: string;
decimals?: number;
}
export interface LensLineChartConfig {
extraVisualizationState?: Partial<Omit<XYState, 'layers'> & { layers: XYState['layers'] }>;
extraLayers?: FormBasedPersistedState['layers'];
extraReference?: string;
}
export interface LensChartConfig {
title: string;
formula: Formula;
lineChartConfig?: LensLineChartConfig;
getFilters: ({ id }: Pick<DataViewBase, 'id'>) => Filter[];
}
export type TVisualization = XYState | MetricVisualizationState;
export interface VisualizationAttributes<T extends TVisualization> {
getTitle(): string;
getVisualizationType(): string;
getLayers(): FormBasedPersistedState['layers'];
getVisualizationState(): T;
getReferences(): SavedObjectReference[];
getFilters(): Filter[];
getDataView(): DataView;
}
// Attributes
export type LensVisualizationState = XYState | MetricVisualizationState;
export interface VisualizationAttributesBuilder {
build(): LensAttributes;
}
export type Formula = Parameters<FormulaPublicApi['insertOrReplaceFormulaColumn']>[1];
// Column
export interface ChartColumn {
getData(
id: string,
baseLayer: PersistedIndexPatternLayer,
dataView: DataView
): PersistedIndexPatternLayer;
getFormulaConfig(): FormulaConfig;
}
// Layer
export type LensLayerConfig = XYDataLayerConfig | MetricVisualizationState;
export interface ChartLayer<TLayerConfig extends LensLayerConfig> {
getName(): string | undefined;
getLayer(
layerId: string,
accessorId: string,
dataView: DataView
): FormBasedPersistedState['layers'];
getReference(layerId: string, dataView: DataView): SavedObjectReference[];
getLayerConfig(layerId: string, acessorId: string): TLayerConfig;
}
// Chart
export interface Chart<TVisualizationState extends LensVisualizationState> {
getTitle(): string;
getVisualizationType(): string;
getLayers(): FormBasedPersistedState['layers'];
getVisualizationState(): TVisualizationState;
getReferences(): SavedObjectReference[];
getDataView(): DataView;
}
export interface ChartConfig<
TLayer extends ChartLayer<LensLayerConfig> | Array<ChartLayer<LensLayerConfig>>
> {
dataView: DataView;
layers: TLayer;
title?: string;
}
// Formula
type LensFormula = Parameters<FormulaPublicApi['insertOrReplaceFormulaColumn']>[1];
export interface FormulaConfig {
label?: string;
color?: string;
format: NonNullable<LensFormula['format']>;
value: string;
}
export type VisualizationTypes = keyof typeof visualizationTypes;
export type HostsLensFormulas = keyof typeof hostLensFormulas;
export type HostsLensMetricChartFormulas = Exclude<HostsLensFormulas, 'diskIORead' | 'diskIOWrite'>;
export type HostsLensLineChartFormulas = Exclude<HostsLensFormulas, 'hostCount'>;

View file

@ -8,7 +8,7 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { Tile } from './tile';
import { KPI_CHARTS } from '../../../../common/visualizations/lens/kpi_grid_config';
import { KPI_CHARTS } from '../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import type { KPIProps } from './overview';
import type { StringDateRange } from '../../types';
@ -27,8 +27,8 @@ export const KPIGrid = React.memo(({ nodeName, dataView, dateRange }: KPIGridPro
style={{ flexGrow: 0 }}
data-test-subj="assetDetailsKPIGrid"
>
{KPI_CHARTS.map(({ ...chartProp }) => (
<EuiFlexItem key={chartProp.type}>
{KPI_CHARTS.map((chartProp, index) => (
<EuiFlexItem key={index}>
<Tile {...chartProp} nodeName={nodeName} dataView={dataView} dateRange={dateRange} />
</EuiFlexItem>
))}

View file

@ -18,25 +18,23 @@ import {
} from '@elastic/eui';
import styled from 'styled-components';
import type { Action } from '@kbn/ui-actions-plugin/public';
import type { KPIChartProps } from '../../../../common/visualizations/lens/kpi_grid_config';
import type { KPIChartProps } from '../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import { useLensAttributes } from '../../../../hooks/use_lens_attributes';
import { LensWrapper } from '../../../../common/visualizations/lens/lens_wrapper';
import { buildCombinedHostsFilter } from '../../../../utils/filters/build';
import { buildCombinedHostsFilter, buildExistsHostsFilter } from '../../../../utils/filters/build';
import { TooltipContent } from '../../../../common/visualizations/metric_explanation/tooltip_content';
import type { KPIGridProps } from './kpi_grid';
const MIN_HEIGHT = 150;
export const Tile = ({
id,
layers,
title,
type,
backgroundColor,
toolTip,
decimals = 1,
trendLine = false,
dataView,
nodeName,
dateRange,
dataView,
}: KPIChartProps & KPIGridProps) => {
const getSubtitle = () =>
i18n.translate('xpack.infra.assetDetailsEmbeddable.overview.metricTrend.subtitle.average', {
@ -44,17 +42,10 @@ export const Tile = ({
});
const { formula, attributes, getExtraActions, error } = useLensAttributes({
type,
dataView,
options: {
backgroundColor,
decimals,
subtitle: getSubtitle(),
showTrendLine: trendLine,
showTitle: false,
title,
},
visualizationType: 'metricChart',
title,
layers: { ...layers, options: { ...layers.options, subtitle: getSubtitle() } },
visualizationType: 'lnsMetric',
});
const filters = useMemo(() => {
@ -64,6 +55,7 @@ export const Tile = ({
values: [nodeName],
dataView,
}),
buildExistsHostsFilter({ field: 'host.name', dataView }),
];
}, [dataView, nodeName]);
@ -83,7 +75,7 @@ export const Tile = ({
hasShadow={false}
paddingSize={error ? 'm' : 'none'}
style={{ minHeight: MIN_HEIGHT }}
data-test-subj={`assetDetailsKPI-${type}`}
data-test-subj={`assetDetailsKPI-${id}`}
>
{error ? (
<EuiFlexGroup
@ -112,7 +104,7 @@ export const Tile = ({
anchorClassName="eui-fullWidth"
>
<LensWrapper
id={`assetDetailsKPIGrid${type}Tile`}
id={`assetDetailsKPIGrid${id}Tile`}
attributes={attributes}
style={{ height: MIN_HEIGHT }}
extraActions={extraActions}

View file

@ -15,6 +15,7 @@ import { CoreStart } from '@kbn/core/public';
import type { InfraClientStartDeps } from '../types';
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
import { FilterStateStore } from '@kbn/es-query';
import { hostLensFormulas } from '../common/visualizations';
jest.mock('@kbn/kibana-react-plugin/public');
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
@ -30,6 +31,8 @@ const mockDataView = {
metaFields: [],
} as unknown as jest.Mocked<DataView>;
const normalizedLoad1m = hostLensFormulas.normalizedLoad1m;
const lensPluginMockStart = lensPluginMock.createStartContract();
const mockUseKibana = () => {
useKibanaMock.mockReturnValue({
@ -48,27 +51,50 @@ describe('useHostTable hook', () => {
it('should return the basic lens attributes', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLensAttributes({
visualizationType: 'lineChart',
type: 'normalizedLoad1m',
options: {
title: 'Injected Normalized Load',
},
visualizationType: 'lnsXY',
layers: [
{
data: [normalizedLoad1m],
layerType: 'data',
options: {
breakdown: {
size: 10,
sourceField: 'host.name',
},
},
},
{
data: [
{
value: '1',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
],
layerType: 'referenceLine',
},
],
title: 'Injected Normalized Load',
dataView: mockDataView,
})
);
await waitForNextUpdate();
const { state, title } = result.current.attributes ?? {};
const { datasourceStates, filters } = state ?? {};
const { datasourceStates } = state ?? {};
expect(title).toBe('Injected Normalized Load');
expect(datasourceStates).toEqual({
formBased: {
layers: {
layer1: {
columnOrder: ['hosts_aggs_breakdown', 'x_date_histogram', 'formula_accessor'],
layer_0: {
columnOrder: ['aggs_breakdown', 'x_date_histogram', 'formula_accessor_0_0'],
columns: {
hosts_aggs_breakdown: {
aggs_breakdown: {
dataType: 'string',
isBucketed: true,
label: 'Top 10 values of host.name',
@ -104,12 +130,12 @@ describe('useHostTable hook', () => {
scale: 'interval',
sourceField: '@timestamp',
},
formula_accessor: {
customLabel: false,
formula_accessor_0_0: {
customLabel: true,
dataType: 'number',
filter: undefined,
isBucketed: false,
label: 'average(system.load.1) / max(system.load.cores)',
label: 'Normalized Load',
operationType: 'formula',
params: {
format: {
@ -128,10 +154,10 @@ describe('useHostTable hook', () => {
},
indexPatternId: 'mock-id',
},
referenceLayer: {
columnOrder: ['referenceColumn'],
layer_1_reference: {
columnOrder: ['formula_accessor_1_0_reference_column'],
columns: {
referenceColumn: {
formula_accessor_1_0_reference_column: {
customLabel: true,
dataType: 'number',
isBucketed: false,
@ -145,7 +171,7 @@ describe('useHostTable hook', () => {
decimals: 0,
},
},
value: 1,
value: '1',
},
references: [],
scale: 'ratio',
@ -158,25 +184,18 @@ describe('useHostTable hook', () => {
},
},
});
expect(filters).toEqual([
{
meta: {
index: 'mock-id',
},
query: {
exists: {
field: 'host.name',
},
},
},
]);
});
it('should return extra actions', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLensAttributes({
visualizationType: 'lineChart',
type: 'normalizedLoad1m',
visualizationType: 'lnsXY',
layers: [
{
data: [normalizedLoad1m],
layerType: 'data',
},
],
dataView: mockDataView,
})
);

View file

@ -12,45 +12,68 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import { i18n } from '@kbn/i18n';
import useAsync from 'react-use/lib/useAsync';
import { FormulaPublicApi, LayerType as LensLayerType } from '@kbn/lens-plugin/public';
import { InfraClientSetupDeps } from '../types';
import {
type HostsLensFormulas,
type HostsLensMetricChartFormulas,
type HostsLensLineChartFormulas,
type LineChartOptions,
type MetricChartOptions,
type XYLayerOptions,
type MetricLayerOptions,
type FormulaConfig,
type LensAttributes,
LensAttributesBuilder,
LensAttributes,
hostLensFormulas,
visualizationTypes,
XYDataLayer,
MetricLayer,
XYChart,
MetricChart,
XYReferenceLinesLayer,
Chart,
LensVisualizationState,
} from '../common/visualizations';
import { useLazyRef } from './use_lazy_ref';
type Options = LineChartOptions | MetricChartOptions;
interface UseLensAttributesBaseParams<T extends HostsLensFormulas, O extends Options> {
dataView?: DataView;
type: T;
options?: O;
type Options = XYLayerOptions | MetricLayerOptions;
type ChartType = 'lnsXY' | 'lnsMetric';
export type LayerType = Exclude<LensLayerType, 'annotations' | 'metricTrendline'>;
export interface Layer<
TOptions extends Options,
TFormulaConfig extends FormulaConfig | FormulaConfig[],
TLayerType extends LayerType = LayerType
> {
layerType: TLayerType;
data: TFormulaConfig;
options?: TOptions;
}
interface UseLensAttributesLineChartParams
extends UseLensAttributesBaseParams<HostsLensLineChartFormulas, LineChartOptions> {
visualizationType: 'lineChart';
interface UseLensAttributesBaseParams<
TOptions extends Options,
TLayers extends Array<Layer<TOptions, FormulaConfig[]>> | Layer<TOptions, FormulaConfig>
> {
dataView?: DataView;
layers: TLayers;
title?: string;
}
interface UseLensAttributesXYChartParams
extends UseLensAttributesBaseParams<
XYLayerOptions,
Array<Layer<XYLayerOptions, FormulaConfig[], 'data' | 'referenceLine'>>
> {
visualizationType: 'lnsXY';
}
interface UseLensAttributesMetricChartParams
extends UseLensAttributesBaseParams<HostsLensMetricChartFormulas, MetricChartOptions> {
visualizationType: 'metricChart';
extends UseLensAttributesBaseParams<
MetricLayerOptions,
Layer<MetricLayerOptions, FormulaConfig, 'data'>
> {
visualizationType: 'lnsMetric';
}
type UseLensAttributesParams =
| UseLensAttributesLineChartParams
| UseLensAttributesMetricChartParams;
type UseLensAttributesParams = UseLensAttributesXYChartParams | UseLensAttributesMetricChartParams;
export const useLensAttributes = ({
type,
dataView,
options,
layers,
title,
visualizationType,
}: UseLensAttributesParams) => {
const {
@ -60,29 +83,26 @@ export const useLensAttributes = ({
const { value, error } = useAsync(lens.stateHelperApi, [lens]);
const { formula: formulaAPI } = value ?? {};
const lensChartConfig = hostLensFormulas[type];
const Chart = visualizationTypes[visualizationType];
const attributes = useLazyRef(() => {
if (!dataView || !formulaAPI) {
return null;
}
const builder = new LensAttributesBuilder(
new Chart(lensChartConfig, dataView, formulaAPI, options)
);
const builder = new LensAttributesBuilder({
visualization: chartFactory({
dataView,
formulaAPI,
layers,
title,
visualizationType,
}),
});
return builder.build();
});
const injectFilters = useCallback(
({
filters,
query = { language: 'kuery', query: '' },
}: {
filters: Filter[];
query?: Query;
}): LensAttributes | null => {
({ filters, query }: { filters: Filter[]; query: Query }): LensAttributes | null => {
if (!attributes.current) {
return null;
}
@ -99,7 +119,7 @@ export const useLensAttributes = ({
);
const openInLensAction = useCallback(
({ timeRange, filters, query }: { timeRange: TimeRange; filters: Filter[]; query?: Query }) =>
({ timeRange, query, filters }: { timeRange: TimeRange; filters: Filter[]; query: Query }) =>
() => {
const injectedAttributes = injectFilters({ filters, query });
if (injectedAttributes) {
@ -119,18 +139,105 @@ export const useLensAttributes = ({
);
const getExtraActions = useCallback(
({ timeRange, filters, query }: { timeRange: TimeRange; filters: Filter[]; query?: Query }) => {
({
timeRange,
filters = [],
query = { language: 'kuery', query: '' },
}: {
timeRange: TimeRange;
filters?: Filter[];
query?: Query;
}) => {
const openInLens = getOpenInLensAction(openInLensAction({ timeRange, filters, query }));
return [openInLens];
},
[openInLensAction]
);
const {
formula: { formula },
} = lensChartConfig;
const getFormula = () => {
const firstDataLayer = [...(Array.isArray(layers) ? layers : [layers])].find(
(p) => p.layerType === 'data'
);
return { formula, attributes: attributes.current, getExtraActions, error };
if (!firstDataLayer) {
return '';
}
const mainFormulaConfig = Array.isArray(firstDataLayer.data)
? firstDataLayer.data[0]
: firstDataLayer.data;
return mainFormulaConfig.value;
};
return { formula: getFormula(), attributes: attributes.current, getExtraActions, error };
};
const chartFactory = <
TOptions,
TLayers extends Array<Layer<TOptions, FormulaConfig[]>> | Layer<TOptions, FormulaConfig>
>({
dataView,
formulaAPI,
layers,
title,
visualizationType,
}: {
dataView: DataView;
formulaAPI: FormulaPublicApi;
visualizationType: ChartType;
layers: TLayers;
title?: string;
}): Chart<LensVisualizationState> => {
switch (visualizationType) {
case 'lnsXY':
if (!Array.isArray(layers)) {
throw new Error(`Invalid layers type. Expected an array of layers.`);
}
const getLayerClass = (layerType: LayerType) => {
switch (layerType) {
case 'data': {
return XYDataLayer;
}
case 'referenceLine': {
return XYReferenceLinesLayer;
}
default:
throw new Error(`Invalid layerType: ${layerType}`);
}
};
return new XYChart({
dataView,
layers: layers.map((layerItem) => {
const Layer = getLayerClass(layerItem.layerType);
return new Layer({
data: layerItem.data,
formulaAPI,
options: layerItem.options,
});
}),
title,
});
case 'lnsMetric':
if (Array.isArray(layers)) {
throw new Error(`Invalid layers type. Expected a single layer object.`);
}
return new MetricChart({
dataView,
layers: new MetricLayer({
data: layers.data,
formulaAPI,
options: layers.options,
}),
title,
});
default:
throw new Error(`Unsupported chart type: ${visualizationType}`);
}
};
const getOpenInLensAction = (onExecute: () => void): Action => {

View file

@ -6,14 +6,14 @@
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import { KPIChartProps } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import { hostLensFormulas } from '../../../../../common/visualizations';
import { useHostCountContext } from '../../hooks/use_host_count';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';
import { TOOLTIP } from '../../../../../common/visualizations/lens/translations';
import { TOOLTIP } from '../../../../../common/visualizations/lens/dashboards/host/translations';
import { type Props, MetricChartWrapper } from '../chart/metric_chart_wrapper';
import { TooltipContent } from '../../../../../common/visualizations/metric_explanation/tooltip_content';
import { KPIChartProps } from './tile';
const HOSTS_CHART: Omit<Props, 'loading' | 'value' | 'toolTip'> = {
id: `metric-hostCount`,
@ -47,7 +47,7 @@ export const HostsTile = ({ style }: Pick<KPIChartProps, 'style'>) => {
subtitle={getSubtitle()}
toolTip={
<TooltipContent
formula={hostLensFormulas.hostCount.formula.formula}
formula={hostLensFormulas.hostCount.value}
description={TOOLTIP.hostCount}
/>
}

View file

@ -13,7 +13,7 @@ import { Tile } from './tile';
import { HostCountProvider } from '../../hooks/use_host_count';
import { HostsTile } from './hosts_tile';
import { KPI_CHART_MIN_HEIGHT } from '../../constants';
import { KPI_CHARTS } from '../../../../../common/visualizations/lens/kpi_grid_config';
import { KPI_CHARTS } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
const lensStyle: CSSProperties = {
height: KPI_CHART_MIN_HEIGHT,
@ -33,8 +33,8 @@ export const KPIGrid = () => {
<EuiFlexItem>
<HostsTile style={lensStyle} />
</EuiFlexItem>
{KPI_CHARTS.map(({ ...chartProp }) => (
<EuiFlexItem key={chartProp.type}>
{KPI_CHARTS.map((chartProp, index) => (
<EuiFlexItem key={index}>
<Tile {...chartProp} style={lensStyle} />
</EuiFlexItem>
))}

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { CSSProperties, useMemo, useCallback } from 'react';
import React, { useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { BrushTriggerEvent } from '@kbn/charts-plugin/public';
@ -19,38 +19,22 @@ import {
} from '@elastic/eui';
import styled from 'styled-components';
import { Action } from '@kbn/ui-actions-plugin/public';
import { KPIChartProps } from '../../../../../common/visualizations/lens/dashboards/host/kpi_grid_config';
import {
buildCombinedHostsFilter,
buildExistsHostsFilter,
} from '../../../../../utils/filters/build';
import { useLensAttributes } from '../../../../../hooks/use_lens_attributes';
import { useMetricsDataViewContext } from '../../hooks/use_data_view';
import { useUnifiedSearchContext } from '../../hooks/use_unified_search';
import { HostsLensMetricChartFormulas } from '../../../../../common/visualizations';
import { useHostsViewContext } from '../../hooks/use_hosts_view';
import { LensWrapper } from '../../../../../common/visualizations/lens/lens_wrapper';
import { buildCombinedHostsFilter } from '../../../../../utils/filters/build';
import { useHostCountContext } from '../../hooks/use_host_count';
import { useAfterLoadedState } from '../../hooks/use_after_loaded_state';
import { TooltipContent } from '../../../../../common/visualizations/metric_explanation/tooltip_content';
import { KPI_CHART_MIN_HEIGHT } from '../../constants';
export interface KPIChartProps {
title: string;
subtitle?: string;
trendLine?: boolean;
backgroundColor: string;
type: HostsLensMetricChartFormulas;
decimals?: number;
toolTip: string;
style?: CSSProperties;
}
export const Tile = ({
title,
type,
backgroundColor,
toolTip,
style,
decimals = 1,
trendLine = false,
}: KPIChartProps) => {
export const Tile = ({ id, title, layers, style, toolTip, ...props }: KPIChartProps) => {
const { searchCriteria, onSubmit } = useUnifiedSearchContext();
const { dataView } = useMetricsDataViewContext();
const { requestTs, hostNodes, loading: hostsLoading } = useHostsViewContext();
@ -70,17 +54,10 @@ export const Tile = ({
};
const { formula, attributes, getExtraActions, error } = useLensAttributes({
type,
dataView,
options: {
backgroundColor,
decimals,
subtitle: getSubtitle(),
showTrendLine: trendLine,
showTitle: false,
title,
},
visualizationType: 'metricChart',
title,
layers: { ...layers, options: { ...layers.options, subtitle: getSubtitle() } },
visualizationType: 'lnsMetric',
});
const filters = useMemo(() => {
@ -91,6 +68,7 @@ export const Tile = ({
values: hostNodes.map((p) => p.name),
dataView,
}),
buildExistsHostsFilter({ field: 'host.name', dataView }),
];
}, [searchCriteria.filters, hostNodes, dataView]);
@ -133,7 +111,7 @@ export const Tile = ({
<EuiPanelStyled
hasShadow={false}
paddingSize={error ? 'm' : 'none'}
data-test-subj={`hostsViewKPI-${type}`}
data-test-subj={`hostsViewKPI-${id}`}
>
{error ? (
<EuiFlexGroup
@ -163,7 +141,7 @@ export const Tile = ({
>
<div>
<LensWrapper
id={`hostsViewKPIGrid${type}Tile`}
id={`hostsViewKPIGrid${id}Tile`}
attributes={afterLoadedState.attributes}
style={style}
extraActions={extraActions}

View file

@ -17,29 +17,31 @@ import {
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useLensAttributes } from '../../../../../../hooks/use_lens_attributes';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { LensWrapper } from '../../../../../../common/visualizations/lens/lens_wrapper';
import { useLensAttributes, Layer, LayerType } from '../../../../../../hooks/use_lens_attributes';
import { useMetricsDataViewContext } from '../../../hooks/use_data_view';
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import { HostsLensLineChartFormulas } from '../../../../../../common/visualizations';
import { FormulaConfig, XYLayerOptions } from '../../../../../../common/visualizations';
import { useHostsViewContext } from '../../../hooks/use_hosts_view';
import { buildCombinedHostsFilter } from '../../../../../../utils/filters/build';
import {
buildCombinedHostsFilter,
buildExistsHostsFilter,
} from '../../../../../../utils/filters/build';
import { useHostsTableContext } from '../../../hooks/use_hosts_table';
import { LensWrapper } from '../../../../../../common/visualizations/lens/lens_wrapper';
import { useAfterLoadedState } from '../../../hooks/use_after_loaded_state';
import { METRIC_CHART_MIN_HEIGHT } from '../../../constants';
export interface MetricChartProps {
export interface MetricChartProps extends Pick<TypedLensByValueInput, 'id' | 'overrides'> {
title: string;
type: HostsLensLineChartFormulas;
breakdownSize: number;
render?: boolean;
layers: Array<Layer<XYLayerOptions, FormulaConfig[], LayerType>>;
}
const lensStyle: CSSProperties = {
height: METRIC_CHART_MIN_HEIGHT,
};
export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => {
export const MetricChart = ({ id, title, layers, overrides }: MetricChartProps) => {
const { euiTheme } = useEuiTheme();
const { searchCriteria, onSubmit } = useUnifiedSearchContext();
const { dataView } = useMetricsDataViewContext();
@ -54,13 +56,10 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) =>
});
const { attributes, getExtraActions, error } = useLensAttributes({
type,
dataView,
options: {
title,
breakdownSize,
},
visualizationType: 'lineChart',
layers,
title,
visualizationType: 'lnsXY',
});
const filters = useMemo(() => {
@ -71,6 +70,7 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) =>
values: currentPage.map((p) => p.name),
dataView,
}),
buildExistsHostsFilter({ field: 'host.name', dataView }),
];
}, [currentPage, dataView, searchCriteria.filters]);
@ -108,7 +108,7 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) =>
min-height: calc(${METRIC_CHART_MIN_HEIGHT}px + ${euiTheme.size.l});
position: relative;
`}
data-test-subj={`hostsView-metricChart-${type}`}
data-test-subj={`hostsView-metricChart-${id}`}
>
{error ? (
<EuiFlexGroup
@ -132,7 +132,7 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) =>
</EuiFlexGroup>
) : (
<LensWrapper
id={`hostsViewsmetricsChart-${type}`}
id={`hostsViewsmetricsChart-${id}`}
attributes={attributes}
style={lensStyle}
extraActions={extraActions}
@ -142,6 +142,7 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) =>
query={afterLoadedState.query}
onBrushEnd={handleBrushEnd}
loading={loading}
overrides={overrides}
hasTitle
/>
)}

View file

@ -9,82 +9,201 @@ import React from 'react';
import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiSpacer } from '@elastic/eui';
import { hostLensFormulas, type XYLayerOptions } from '../../../../../../common/visualizations';
import { HostMetricsDocsLink } from '../../../../../../common/visualizations/metric_explanation/host_metrics_docs_link';
import { MetricChart, MetricChartProps } from './metric_chart';
const DEFAULT_BREAKDOWN_SIZE = 20;
const CHARTS_IN_ORDER: Array<Pick<MetricChartProps, 'title' | 'type'> & { fullRow?: boolean }> = [
const XY_LAYER_OPTIONS: XYLayerOptions = {
breakdown: {
size: DEFAULT_BREAKDOWN_SIZE,
sourceField: 'host.name',
},
};
const PERCENT_LEFT_AXIS: Pick<MetricChartProps, 'overrides'>['overrides'] = {
axisLeft: {
domain: {
min: 0,
max: 1,
},
},
};
const CHARTS_IN_ORDER: MetricChartProps[] = [
{
id: 'cpuUsage',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.cpuUsage', {
defaultMessage: 'CPU Usage',
}),
type: 'cpuUsage',
layers: [
{
data: [hostLensFormulas.cpuUsage],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
overrides: PERCENT_LEFT_AXIS,
},
{
id: 'normalizedLoad1m',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.normalizedLoad1m', {
defaultMessage: 'Normalized Load',
}),
type: 'normalizedLoad1m',
layers: [
{
data: [hostLensFormulas.normalizedLoad1m],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
{
data: [
{
value: '1',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
color: '#6092c0',
},
],
layerType: 'referenceLine',
},
],
},
{
id: 'memoryUsage',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.memoryUsage', {
defaultMessage: 'Memory Usage',
}),
type: 'memoryUsage',
layers: [
{
data: [hostLensFormulas.memoryUsage],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
overrides: PERCENT_LEFT_AXIS,
},
{
id: 'memoryFree',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.memoryFree', {
defaultMessage: 'Memory Free',
}),
type: 'memoryFree',
layers: [
{
data: [hostLensFormulas.memoryFree],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
{
id: 'diskSpaceUsed',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.diskSpaceUsed', {
defaultMessage: 'Disk Space Usage',
}),
type: 'diskSpaceUsage',
layers: [
{
data: [hostLensFormulas.diskSpaceUsage],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
overrides: PERCENT_LEFT_AXIS,
},
{
id: 'diskSpaceAvailable',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.diskSpaceAvailable', {
defaultMessage: 'Disk Space Available',
}),
type: 'diskSpaceAvailable',
layers: [
{
data: [hostLensFormulas.diskSpaceAvailable],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
{
id: 'diskIORead',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.diskIORead', {
defaultMessage: 'Disk Read IOPS',
}),
type: 'diskIORead',
layers: [
{
data: [hostLensFormulas.diskIORead],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
{
id: 'diskIOWrite',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.diskIOWrite', {
defaultMessage: 'Disk Write IOPS',
}),
type: 'diskIOWrite',
layers: [
{
data: [hostLensFormulas.diskIOWrite],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
{
id: 'diskReadThroughput',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.diskReadThroughput', {
defaultMessage: 'Disk Read Throughput',
}),
type: 'diskReadThroughput',
layers: [
{
data: [hostLensFormulas.diskReadThroughput],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
{
id: 'diskWriteThroughput',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.diskWriteThroughput', {
defaultMessage: 'Disk Write Throughput',
}),
type: 'diskWriteThroughput',
layers: [
{
data: [hostLensFormulas.diskWriteThroughput],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
{
id: 'rx',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.rx', {
defaultMessage: 'Network Inbound (RX)',
}),
type: 'rx',
layers: [
{
data: [hostLensFormulas.rx],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
{
id: 'tx',
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.tx', {
defaultMessage: 'Network Outbound (TX)',
}),
type: 'tx',
layers: [
{
data: [hostLensFormulas.tx],
layerType: 'data',
options: XY_LAYER_OPTIONS,
},
],
},
];
@ -94,9 +213,9 @@ export const MetricsGrid = React.memo(() => {
<HostMetricsDocsLink />
<EuiSpacer size="s" />
<EuiFlexGrid columns={2} gutterSize="s" data-test-subj="hostsView-metricChart">
{CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => (
<EuiFlexItem key={chartProp.type} style={fullRow ? { gridColumn: '1/-1' } : {}}>
<MetricChart breakdownSize={DEFAULT_BREAKDOWN_SIZE} {...chartProp} />
{CHARTS_IN_ORDER.map((chartProp, index) => (
<EuiFlexItem key={index}>
<MetricChart {...chartProp} />
</EuiFlexItem>
))}
</EuiFlexGrid>

View file

@ -31,7 +31,7 @@ import { useUnifiedSearchContext } from './use_unified_search';
import { useMetricsDataViewContext } from './use_data_view';
import { ColumnHeader } from '../components/table/column_header';
import { TABLE_COLUMN_LABEL } from '../translations';
import { TOOLTIP } from '../../../../common/visualizations/lens/translations';
import { TOOLTIP } from '../../../../common/visualizations/lens/dashboards/host/translations';
import { buildCombinedHostsFilter } from '../../../../utils/filters/build';
/**
@ -255,7 +255,7 @@ export const useHostsTable = () => {
<ColumnHeader
label={TABLE_COLUMN_LABEL.cpuUsage}
toolTip={TOOLTIP.cpuUsage}
formula={hostLensFormulas.cpuUsage.formula.formula}
formula={hostLensFormulas.cpuUsage.value}
popoverContainerRef={popoverContainerRef}
/>
),
@ -270,7 +270,7 @@ export const useHostsTable = () => {
<ColumnHeader
label={TABLE_COLUMN_LABEL.normalizedLoad1m}
toolTip={TOOLTIP.normalizedLoad1m}
formula={hostLensFormulas.normalizedLoad1m.formula.formula}
formula={hostLensFormulas.normalizedLoad1m.value}
popoverContainerRef={popoverContainerRef}
/>
),
@ -285,7 +285,7 @@ export const useHostsTable = () => {
<ColumnHeader
label={TABLE_COLUMN_LABEL.memoryUsage}
toolTip={TOOLTIP.memoryUsage}
formula={hostLensFormulas.memoryUsage.formula.formula}
formula={hostLensFormulas.memoryUsage.value}
popoverContainerRef={popoverContainerRef}
/>
),
@ -300,7 +300,7 @@ export const useHostsTable = () => {
<ColumnHeader
label={TABLE_COLUMN_LABEL.memoryFree}
toolTip={TOOLTIP.memoryFree}
formula={hostLensFormulas.memoryFree.formula.formula}
formula={hostLensFormulas.memoryFree.value}
popoverContainerRef={popoverContainerRef}
/>
),
@ -315,7 +315,7 @@ export const useHostsTable = () => {
<ColumnHeader
label={TABLE_COLUMN_LABEL.diskSpaceUsage}
toolTip={TOOLTIP.diskSpaceUsage}
formula={hostLensFormulas.diskSpaceUsage.formula.formula}
formula={hostLensFormulas.diskSpaceUsage.value}
popoverContainerRef={popoverContainerRef}
/>
),
@ -330,7 +330,7 @@ export const useHostsTable = () => {
<ColumnHeader
label={TABLE_COLUMN_LABEL.rx}
toolTip={TOOLTIP.rx}
formula={hostLensFormulas.rx.formula.formula}
formula={hostLensFormulas.rx.value}
popoverContainerRef={popoverContainerRef}
/>
),
@ -346,7 +346,7 @@ export const useHostsTable = () => {
<ColumnHeader
label={TABLE_COLUMN_LABEL.tx}
toolTip={TOOLTIP.tx}
formula={hostLensFormulas.tx.formula.formula}
formula={hostLensFormulas.tx.value}
popoverContainerRef={popoverContainerRef}
/>
),

View file

@ -9,11 +9,33 @@ import {
BooleanRelation,
buildCombinedFilter,
buildPhraseFilter,
buildExistsFilter,
Filter,
isCombinedFilter,
} from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/common';
export const buildExistsHostsFilter = ({
field,
dataView,
}: {
field: string;
dataView?: DataView;
}) => {
if (!dataView) {
return {
meta: {},
query: {
exists: {
field,
},
},
};
}
const indexField = dataView.getFieldByName(field)!;
return buildExistsFilter(indexField, dataView);
};
export const buildCombinedHostsFilter = ({
field,
values,
@ -27,7 +49,7 @@ export const buildCombinedHostsFilter = ({
return {
query: {
terms: {
'host.name': values,
[field]: values,
},
},
meta: {},