diff --git a/x-pack/plugins/infra/public/common/visualizations/constants.ts b/x-pack/plugins/infra/public/common/visualizations/constants.ts index 66f0cb1e1c422..a91a29e2c44e3 100644 --- a/x-pack/plugins/infra/public/common/visualizations/constants.ts +++ b/x-pack/plugins/infra/public/common/visualizations/constants.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/common/visualizations/index.ts b/x-pack/plugins/infra/public/common/visualizations/index.ts index 9be4015a80a73..888bf3d8f8ad6 100644 --- a/x-pack/plugins/infra/public/common/visualizations/index.ts +++ b/x-pack/plugins/infra/public/common/visualizations/index.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/README.md b/x-pack/plugins/infra/public/common/visualizations/lens/README.md new file mode 100644 index 0000000000000..c0fa8340f180e --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/README.md @@ -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(); + + +``` diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts b/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts new file mode 100644 index 0000000000000..cc4f51c8f2d18 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/kpi_grid_config.ts @@ -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 { + layers: Layer; + 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, + }, +]; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/translations.ts b/x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/translations.ts similarity index 100% rename from x-pack/plugins/infra/public/common/visualizations/lens/translations.ts rename to x-pack/plugins/infra/public/common/visualizations/lens/dashboards/host/translations.ts diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts index f0e8813d2aef4..b3b40f585d7e0 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/cpu_usage.ts @@ -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, -}; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts index 59879681b641c..27b288f3a119e 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_iops.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts index 557a6ec0249e7..946e26cec62a1 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_read_throughput.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts index 77fcabf3c710f..088e28799ce03 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_available.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts index 9599e65d4de9b..e4cb5851d5241 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_space_usage.ts @@ -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, -}; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts index 67d697551c44a..04370c61903ce 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_iops.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts index 97e223036af7f..e391bce4a1151 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/disk_write_throughput.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts index 4f0d230176368..e642a8cb629f1 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/host_count.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts index 7821ef7a12ff1..4406ebd1e820c 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_free.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts index bc76d88053dd9..f95198756d61e 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/memory_usage.ts @@ -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, -}; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts index 5e04919740689..32031d07fb858 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/normalized_load_1m.ts @@ -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: {}, - }, - }, - 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, - }, +export const normalizedLoad1m: FormulaConfig = { + label: 'Normalized Load', + value: 'average(system.load.1) / max(system.load.cores)', + format: { + id: 'percent', + params: { + decimals: 0, }, }, - getFilters, - lineChartConfig: loadLineChart, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts index b396dffb979e6..2c5da5cb83988 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/rx.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts index f9f97a8ed9112..70aa43a4efaf0 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/tx.ts @@ -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, }; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/utils.ts b/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/utils.ts deleted file mode 100644 index c0b16ca705b13..0000000000000 --- a/x-pack/plugins/infra/public/common/visualizations/lens/formulas/host/utils.ts +++ /dev/null @@ -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) => [ - { - meta: { - index: id, - }, - query: { - exists: { - field: 'host.name', - }, - }, - }, -]; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/kpi_grid_config.ts b/x-pack/plugins/infra/public/common/visualizations/lens/kpi_grid_config.ts deleted file mode 100644 index 6849bceaa7148..0000000000000 --- a/x-pack/plugins/infra/public/common/visualizations/lens/kpi_grid_config.ts +++ /dev/null @@ -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> = [ - { - 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, - }, -]; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.test.ts b/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.test.ts new file mode 100644 index 0000000000000..14d79c41c3829 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.test.ts @@ -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; + +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: [], + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.ts b/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.ts index 90ff3c5cfa268..d873f074ee346 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/lens_attributes_builder.ts @@ -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> +export class LensAttributesBuilder> 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())), }, }; } diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/lens_wrapper.tsx b/x-pack/plugins/infra/public/common/visualizations/lens/lens_wrapper.tsx index 08948d788fbe3..7925966b75429 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/common/visualizations/lens/lens_wrapper.tsx @@ -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( )} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/utils.ts b/x-pack/plugins/infra/public/common/visualizations/lens/utils.ts index 8bbfd6da833fc..e97e87380cd0a 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/utils.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/utils.ts @@ -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, }: { diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/index.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/index.ts index 4abfeb3a60c45..b7112840436de 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/index.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/index.ts @@ -5,5 +5,7 @@ * 2.0. */ -export { LineChart } from './line_chart'; +export { XYChart } from './xy_chart'; export { MetricChart } from './metric_chart'; + +export * from './layers'; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/formula.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/formula.ts new file mode 100644 index 0000000000000..b1e30ce0bd225 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/formula.ts @@ -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; + } +} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/reference_line.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/reference_line.ts new file mode 100644 index 0000000000000..d9f9c5f270997 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/column/reference_line.ts @@ -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: {}, + }; + } +} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/index.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/index.ts new file mode 100644 index 0000000000000..fb8300573b79a --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/index.ts @@ -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'; diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/metric_layer.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/metric_layer.ts new file mode 100644 index 0000000000000..25d05abd0f9c5 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/metric_layer.ts @@ -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 { + 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; + } +} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_data_layer.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_data_layer.ts new file mode 100644 index 0000000000000..b6a8428ee0754 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_data_layer.ts @@ -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 { + 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, + }; + } +} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_reference_lines_layer.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_reference_lines_layer.ts new file mode 100644 index 0000000000000..6508dcbc2cf49 --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/layers/xy_reference_lines_layer.ts @@ -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 { + 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', + })), + }; + } +} diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/line_chart.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/line_chart.ts deleted file mode 100644 index bf1f245dd0447..0000000000000 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/line_chart.ts +++ /dev/null @@ -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 { - 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, '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, -}); diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/metric_chart.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/metric_chart.ts index 67b944992e566..ecdb8fbb50480 100644 --- a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/metric_chart.ts +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/metric_chart.ts @@ -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 { - constructor( - private chartConfig: LensChartConfig, - private dataView: DataView, - private formulaAPI: FormulaPublicApi, - private options?: MetricChartOptions - ) {} +export class MetricChart implements Chart { + constructor(private chartConfig: ChartConfig>) {} 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 ?? ''; } } diff --git a/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/xy_chart.ts b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/xy_chart.ts new file mode 100644 index 0000000000000..2794a86b3499b --- /dev/null +++ b/x-pack/plugins/infra/public/common/visualizations/lens/visualization_types/xy_chart.ts @@ -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 { + constructor(private chartConfig: ChartConfig>>) {} + + 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, '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, +}); diff --git a/x-pack/plugins/infra/public/common/visualizations/types.ts b/x-pack/plugins/infra/public/common/visualizations/types.ts index 969016f0b1bcf..b3d2a3815ac6d 100644 --- a/x-pack/plugins/infra/public/common/visualizations/types.ts +++ b/x-pack/plugins/infra/public/common/visualizations/types.ts @@ -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; +// Attributes +export type LensVisualizationState = XYState | MetricVisualizationState; + +export interface VisualizationAttributesBuilder { + build(): LensAttributes; } -export interface LensLineChartConfig { - extraVisualizationState?: Partial & { layers: XYState['layers'] }>; - extraLayers?: FormBasedPersistedState['layers']; - extraReference?: string; +// Column +export interface ChartColumn { + getData( + id: string, + baseLayer: PersistedIndexPatternLayer, + dataView: DataView + ): PersistedIndexPatternLayer; + getFormulaConfig(): FormulaConfig; } -export interface LensChartConfig { - title: string; - formula: Formula; - lineChartConfig?: LensLineChartConfig; - getFilters: ({ id }: Pick) => Filter[]; + +// Layer +export type LensLayerConfig = XYDataLayerConfig | MetricVisualizationState; + +export interface ChartLayer { + getName(): string | undefined; + getLayer( + layerId: string, + accessorId: string, + dataView: DataView + ): FormBasedPersistedState['layers']; + getReference(layerId: string, dataView: DataView): SavedObjectReference[]; + getLayerConfig(layerId: string, acessorId: string): TLayerConfig; } -export type TVisualization = XYState | MetricVisualizationState; -export interface VisualizationAttributes { +// Chart +export interface Chart { getTitle(): string; getVisualizationType(): string; getLayers(): FormBasedPersistedState['layers']; - getVisualizationState(): T; + getVisualizationState(): TVisualizationState; getReferences(): SavedObjectReference[]; - getFilters(): Filter[]; getDataView(): DataView; } - -export interface VisualizationAttributesBuilder { - build(): LensAttributes; +export interface ChartConfig< + TLayer extends ChartLayer | Array> +> { + dataView: DataView; + layers: TLayer; + title?: string; } -export type Formula = Parameters[1]; +// Formula +type LensFormula = Parameters[1]; +export interface FormulaConfig { + label?: string; + color?: string; + format: NonNullable; + value: string; +} -export type VisualizationTypes = keyof typeof visualizationTypes; export type HostsLensFormulas = keyof typeof hostLensFormulas; export type HostsLensMetricChartFormulas = Exclude; export type HostsLensLineChartFormulas = Exclude; diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpi_grid.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpi_grid.tsx index 89bb6b636a98e..c368413ba2864 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/kpi_grid.tsx @@ -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 }) => ( - + {KPI_CHARTS.map((chartProp, index) => ( + ))} diff --git a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/tile.tsx b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/tile.tsx index 18ae64ad8ee6a..25dc51b221d4f 100644 --- a/x-pack/plugins/infra/public/components/asset_details/tabs/overview/tile.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/tabs/overview/tile.tsx @@ -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 ? ( ; @@ -30,6 +31,8 @@ const mockDataView = { metaFields: [], } as unknown as jest.Mocked; +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, }) ); diff --git a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts index ac7ec3387e8e7..721bf5669d203 100644 --- a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts @@ -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 { +type Options = XYLayerOptions | MetricLayerOptions; +type ChartType = 'lnsXY' | 'lnsMetric'; +export type LayerType = Exclude; +export interface Layer< + TOptions extends Options, + TFormulaConfig extends FormulaConfig | FormulaConfig[], + TLayerType extends LayerType = LayerType +> { + layerType: TLayerType; + data: TFormulaConfig; + options?: TOptions; +} + +interface UseLensAttributesBaseParams< + TOptions extends Options, + TLayers extends Array> | Layer +> { dataView?: DataView; - type: T; - options?: O; + layers: TLayers; + title?: string; } -interface UseLensAttributesLineChartParams - extends UseLensAttributesBaseParams { - visualizationType: 'lineChart'; +interface UseLensAttributesXYChartParams + extends UseLensAttributesBaseParams< + XYLayerOptions, + Array> + > { + visualizationType: 'lnsXY'; } interface UseLensAttributesMetricChartParams - extends UseLensAttributesBaseParams { - visualizationType: 'metricChart'; + extends UseLensAttributesBaseParams< + MetricLayerOptions, + Layer + > { + 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' + ); + + if (!firstDataLayer) { + return ''; + } + + const mainFormulaConfig = Array.isArray(firstDataLayer.data) + ? firstDataLayer.data[0] + : firstDataLayer.data; - return { formula, attributes: attributes.current, getExtraActions, error }; + return mainFormulaConfig.value; + }; + + return { formula: getFormula(), attributes: attributes.current, getExtraActions, error }; +}; + +const chartFactory = < + TOptions, + TLayers extends Array> | Layer +>({ + dataView, + formulaAPI, + layers, + title, + visualizationType, +}: { + dataView: DataView; + formulaAPI: FormulaPublicApi; + visualizationType: ChartType; + layers: TLayers; + title?: string; +}): Chart => { + 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 => { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx index 9cfb67a88c3b0..806570ebac349 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx @@ -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 = { id: `metric-hostCount`, @@ -47,7 +47,7 @@ export const HostsTile = ({ style }: Pick) => { subtitle={getSubtitle()} toolTip={ } diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index 5914be028ab8b..01fd3ccef909f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -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 = () => { - {KPI_CHARTS.map(({ ...chartProp }) => ( - + {KPI_CHARTS.map((chartProp, index) => ( + ))} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index 5890442358113..e9d031401f7d1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -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 = ({ {error ? (
{ title: string; - type: HostsLensLineChartFormulas; - breakdownSize: number; - render?: boolean; + layers: Array>; } 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 ? ( ) : ( query={afterLoadedState.query} onBrushEnd={handleBrushEnd} loading={loading} + overrides={overrides} hasTitle /> )} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx index 4b2ec7a6867e4..47103365fc099 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx @@ -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 & { fullRow?: boolean }> = [ +const XY_LAYER_OPTIONS: XYLayerOptions = { + breakdown: { + size: DEFAULT_BREAKDOWN_SIZE, + sourceField: 'host.name', + }, +}; + +const PERCENT_LEFT_AXIS: Pick['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(() => { - {CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => ( - - + {CHARTS_IN_ORDER.map((chartProp, index) => ( + + ))} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 18310ad2317c0..19c0bcdaa1f47 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -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 = () => { ), @@ -270,7 +270,7 @@ export const useHostsTable = () => { ), @@ -285,7 +285,7 @@ export const useHostsTable = () => { ), @@ -300,7 +300,7 @@ export const useHostsTable = () => { ), @@ -315,7 +315,7 @@ export const useHostsTable = () => { ), @@ -330,7 +330,7 @@ export const useHostsTable = () => { ), @@ -346,7 +346,7 @@ export const useHostsTable = () => { ), diff --git a/x-pack/plugins/infra/public/utils/filters/build.ts b/x-pack/plugins/infra/public/utils/filters/build.ts index eba7b4d8ba032..4a71070ed2c5b 100644 --- a/x-pack/plugins/infra/public/utils/filters/build.ts +++ b/x-pack/plugins/infra/public/utils/filters/build.ts @@ -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: {},