[Infrastructure UI] Add lens charts to hosts view (#148906)

## Summary

closes https://github.com/elastic/obs-infraobs-team/issues/921

This PR introduces a series of lens charts to hosts view page.

![out](https://user-images.githubusercontent.com/2767137/214100703-1847586a-fa87-4c2c-9eb0-af5271667702.gif)

The Lens embeddable component is fully integrated, allowing the filters
to react to clicks on the charts as well as accessing Lens directly from
the chart

The charts will show metrics for:
- Load
- CPU Usage
- RX
- TX
- Memory 
- Disk Read
- Disk Write


### How to test
- Using metricbeat
  - Enable system metric in metricbeat
  - Start your local ES

- Using oblt
  - configure kibana.dev.yml with your oblt-cluster

- Using slingshot
- Clone https://github.com/elastic/slingshot and run slingshot yarn
slingshot load --config ./configs/hosts.json

- Start your local ES
- Start kibana
- Navigate to Infrastructure > Hosts

### Notes
- I've renamed a few things that started to become confusing after this
change.
- The structure created for the lens vis is similar to the
inventory_model, but here is only used in `public` and I've placed them
as a common functionality, since this might also be used in other pages
in the future.

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Carlos Crespo 2023-01-25 10:17:52 +01:00 committed by GitHub
parent ce075504ae
commit 252d81c46a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1974 additions and 135 deletions

View file

@ -12,15 +12,24 @@
"dataViews",
"visTypeTimeseries",
"alerting",
"lens",
"triggersActionsUi",
"observability",
"ruleRegistry",
"unifiedSearch"
],
"optionalPlugins": ["ml", "home", "embeddable", "osquery"],
"optionalPlugins": [
"ml",
"home",
"embeddable",
"osquery"
],
"server": true,
"ui": true,
"configPath": ["xpack", "infra"],
"configPath": [
"xpack",
"infra"
],
"requiredBundles": [
"unifiedSearch",
"observability",
@ -29,11 +38,11 @@
"kibanaReact",
"ml",
"embeddable",
"controls"
"controls"
],
"owner": {
"name": "Logs and Metrics UI",
"githubTeam": "logs-metrics-ui"
},
"description": "This plugin visualizes data from Filebeat and Metricbeat, and integrates with other Observability solutions"
}
}

View file

@ -0,0 +1,22 @@
/*
* 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 { CPU, Load, Memory, RX, TX, DiskIORead, DiskIOWrite } from './lens/hosts';
export { buildLensAttributes } from './lens/lens_visualization';
export const hostMetricsLensAttributes = {
cpu: CPU,
load: Load,
memory: Memory,
rx: RX,
tx: TX,
diskIORead: DiskIORead,
diskIOWrite: DiskIOWrite,
};
export type HostLensAttributesTypes = keyof typeof hostMetricsLensAttributes;

View file

@ -0,0 +1,128 @@
/*
* 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 Filter, FilterStateStore } from '@kbn/es-query';
import type {
FormBasedLayer,
FormulaPublicApi,
PersistedIndexPatternLayer,
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,
getBreakdownColumn,
getHistogramColumn,
getXYVisualizationState,
} from '../utils';
import type { LensOptions } from '../../../../types';
import type { ILensVisualization } from '../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export class CPU implements ILensVisualization {
constructor(
private dataView: DataView,
private options: LensOptions,
private formula: FormulaPublicApi
) {}
getTitle(): string {
return 'CPU Usage';
}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers = (): Record<string, Omit<FormBasedLayer, 'indexPatternId'>> => {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn(BREAKDOWN_COLUMN_NAME, 'host.name', this.options.breakdownSize),
...getHistogramColumn(HISTOGRAM_COLUMN_NAME, this.dataView.timeFieldName ?? '@timestamp'),
},
};
const dataLayer = this.formula.insertOrReplaceFormulaColumn(
'y_cpu_usage',
{
formula: 'average(system.cpu.total.norm.pct)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return { [DEFAULT_LAYER_ID]: dataLayer };
};
getVisualizationState = (): XYState => {
return getXYVisualizationState({
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: ['y_cpu_usage'],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
],
yLeftExtent: {
mode: 'custom',
lowerBound: 0,
upperBound: 1,
},
});
};
getFilters = (): Filter[] => {
return [
{
meta: {
disabled: false,
negate: false,
alias: null,
index: '3be1e71b-4bc5-4462-a314-04539f877a19',
key: 'system.cpu.total.norm.pct',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.cpu.total.norm.pct',
},
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
];
};
getReferences = (): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${DEFAULT_LAYER_ID}`,
},
];
};
}

View file

@ -0,0 +1,123 @@
/*
* 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 Filter, FilterStateStore } from '@kbn/es-query';
import type {
FormBasedLayer,
FormulaPublicApi,
PersistedIndexPatternLayer,
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 type { LensOptions } from '../../../../types';
import {
DEFAULT_LAYER_ID,
getBreakdownColumn,
getHistogramColumn,
getXYVisualizationState,
} from '../utils';
import { ILensVisualization } from '../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export class DiskIORead implements ILensVisualization {
constructor(
private dataView: DataView,
private options: LensOptions,
private formula: FormulaPublicApi
) {}
getTitle(): string {
return 'Disk Read IOPS';
}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers = (): Record<string, Omit<FormBasedLayer, 'indexPatternId'>> => {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn(BREAKDOWN_COLUMN_NAME, 'host.name', this.options.breakdownSize),
...getHistogramColumn(HISTOGRAM_COLUMN_NAME, this.dataView.timeFieldName ?? '@timestamp'),
},
};
const dataLayer = this.formula.insertOrReplaceFormulaColumn(
'y_diskio_read',
{
formula: "counter_rate(max(system.diskio.read.bytes), kql='system.diskio.read.bytes >= 0')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
},
},
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return { [DEFAULT_LAYER_ID]: dataLayer };
};
getVisualizationState = (): XYState => {
return getXYVisualizationState({
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: ['y_diskio_read'],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
],
});
};
getFilters = (): Filter[] => {
return [
{
meta: {
disabled: false,
negate: false,
alias: null,
index: '3be1e71b-4bc5-4462-a314-04539f877a19',
key: 'system.diskio.read.bytes',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.diskio.read.bytes',
},
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
];
};
getReferences = (): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${DEFAULT_LAYER_ID}`,
},
];
};
}

View file

@ -0,0 +1,124 @@
/*
* 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 Filter, FilterStateStore } from '@kbn/es-query';
import type {
FormBasedLayer,
FormulaPublicApi,
PersistedIndexPatternLayer,
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 type { LensOptions } from '../../../../types';
import {
DEFAULT_LAYER_ID,
getBreakdownColumn,
getHistogramColumn,
getXYVisualizationState,
} from '../utils';
import { ILensVisualization } from '../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export class DiskIOWrite implements ILensVisualization {
constructor(
private dataView: DataView,
private options: LensOptions,
private formula: FormulaPublicApi
) {}
getTitle(): string {
return 'Disk Write IOPS';
}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers = (): Record<string, Omit<FormBasedLayer, 'indexPatternId'>> => {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn(BREAKDOWN_COLUMN_NAME, 'host.name', this.options.breakdownSize),
...getHistogramColumn(HISTOGRAM_COLUMN_NAME, this.dataView.timeFieldName ?? '@timestamp'),
},
};
const dataLayer = this.formula.insertOrReplaceFormulaColumn(
'y_diskio_write',
{
formula:
"counter_rate(max(system.diskio.write.bytes), kql='system.diskio.write.bytes>= 0')",
format: {
id: 'bytes',
params: {
decimals: 1,
},
},
},
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return { [DEFAULT_LAYER_ID]: dataLayer };
};
getVisualizationState = (): XYState => {
return getXYVisualizationState({
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: ['y_diskio_write'],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
],
});
};
getFilters = (): Filter[] => {
return [
{
meta: {
disabled: false,
negate: false,
alias: null,
index: '3be1e71b-4bc5-4462-a314-04539f877a19',
key: 'system.diskio.write.bytes',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.diskio.write.bytes',
},
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
];
};
getReferences = (): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${DEFAULT_LAYER_ID}`,
},
];
};
}

View file

@ -0,0 +1,14 @@
/*
* 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 { CPU } from './cpu';
export { Load } from './load';
export { Memory } from './memory';
export { RX } from './rx';
export { TX } from './tx';
export { DiskIORead } from './diskio_read';
export { DiskIOWrite } from './diskio_write';

View file

@ -0,0 +1,190 @@
/*
* 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 Filter, FilterStateStore } from '@kbn/es-query';
import type {
PersistedIndexPatternLayer,
FormulaPublicApi,
XYState,
FormBasedLayer,
} from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types';
import {
DEFAULT_LAYER_ID,
getBreakdownColumn,
getHistogramColumn,
getXYVisualizationState,
} from '../utils';
import type { LensOptions } from '../../../../types';
import type { ILensVisualization } from '../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
const REFERENCE_LAYER = 'referenceLayer';
export class Load implements ILensVisualization {
constructor(
private dataView: DataView,
private options: LensOptions,
private formula: FormulaPublicApi
) {}
getTitle(): string {
return 'Normalized Load';
}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers = (): Record<string, Omit<FormBasedLayer, 'indexPatternId'>> => {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn(BREAKDOWN_COLUMN_NAME, 'host.name', this.options.breakdownSize),
...getHistogramColumn(HISTOGRAM_COLUMN_NAME, this.dataView.timeFieldName ?? '@timestamp'),
},
};
const dataLayer = this.formula.insertOrReplaceFormulaColumn(
'y_cpu_cores_usage',
{
formula: 'average(system.load.1) / max(system.load.cores)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return {
[DEFAULT_LAYER_ID]: dataLayer,
referenceLayer: {
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: {},
},
};
};
getVisualizationState = (): XYState => {
return getXYVisualizationState({
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: ['y_cpu_cores_usage'],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
{
layerId: REFERENCE_LAYER,
layerType: 'referenceLine',
accessors: ['referenceColumn'],
yConfig: [
{
forAccessor: 'referenceColumn',
axisMode: 'left',
color: '#6092c0',
},
],
},
],
});
};
getFilters = (): Filter[] => {
return [
{
$state: {
store: FilterStateStore.APP_STATE,
},
meta: {
disabled: false,
negate: false,
alias: null,
index: 'c1ec8212-ecee-494a-80da-f6f33b3393f2',
key: 'system.load.cores',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.load.cores',
},
},
},
{
$state: {
store: FilterStateStore.APP_STATE,
},
meta: {
disabled: false,
negate: false,
alias: null,
index: 'c1ec8212-ecee-494a-80da-f6f33b3393f2',
key: 'system.load.1',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.load.1',
},
},
},
];
};
getReferences = (): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${DEFAULT_LAYER_ID}`,
},
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${REFERENCE_LAYER}`,
},
];
};
}

View file

@ -0,0 +1,146 @@
/*
* 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 Filter, FilterStateStore } from '@kbn/es-query';
import type {
FormBasedLayer,
FormulaPublicApi,
PersistedIndexPatternLayer,
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,
getBreakdownColumn,
getHistogramColumn,
getXYVisualizationState,
} from '../utils';
import type { LensOptions } from '../../../../types';
import { ILensVisualization } from '../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export class Memory implements ILensVisualization {
constructor(
private dataView: DataView,
private options: LensOptions,
private formula: FormulaPublicApi
) {}
getTitle(): string {
return 'Disk Writes IOPS';
}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers = (): Record<string, Omit<FormBasedLayer, 'indexPatternId'>> => {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn(BREAKDOWN_COLUMN_NAME, 'host.name', this.options.breakdownSize),
...getHistogramColumn(HISTOGRAM_COLUMN_NAME, this.dataView.timeFieldName ?? '@timestamp'),
},
};
const dataLayer = this.formula.insertOrReplaceFormulaColumn(
'y_memory_usage',
{
formula: 'average(system.memory.actual.used.bytes) / max(system.memory.total)',
format: {
id: 'percent',
params: {
decimals: 0,
},
},
},
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return { [DEFAULT_LAYER_ID]: dataLayer };
};
getVisualizationState = (): XYState => {
return getXYVisualizationState({
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: ['y_memory_usage'],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
],
yLeftExtent: {
mode: 'custom',
lowerBound: 0,
upperBound: 1,
},
});
};
getFilters = (): Filter[] => {
return [
{
meta: {
disabled: false,
negate: false,
alias: null,
index: '3be1e71b-4bc5-4462-a314-04539f877a19',
key: 'system.memory.actual.used.bytes',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.memory.actual.used.bytes',
},
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
{
meta: {
disabled: false,
negate: false,
alias: null,
index: '3be1e71b-4bc5-4462-a314-04539f877a19',
key: 'system.memory.total',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.memory.total',
},
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
];
};
getReferences = (): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${DEFAULT_LAYER_ID}`,
},
];
};
}

View file

@ -0,0 +1,123 @@
/*
* 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 Filter, FilterStateStore } from '@kbn/es-query';
import type {
FormBasedLayer,
FormulaPublicApi,
PersistedIndexPatternLayer,
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,
getBreakdownColumn,
getHistogramColumn,
getXYVisualizationState,
} from '../utils';
import type { LensOptions } from '../../../../types';
import { ILensVisualization } from '../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export class RX implements ILensVisualization {
constructor(
private dataView: DataView,
private options: LensOptions,
private formula: FormulaPublicApi
) {}
getTitle(): string {
return 'Network Inbound (RX)';
}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers = (): Record<string, Omit<FormBasedLayer, 'indexPatternId'>> => {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn(BREAKDOWN_COLUMN_NAME, 'host.name', this.options.breakdownSize),
...getHistogramColumn(HISTOGRAM_COLUMN_NAME, this.dataView.timeFieldName ?? '@timestamp'),
},
};
const dataLayer = this.formula.insertOrReplaceFormulaColumn(
'y_network_in_bytes',
{
formula: "counter_rate(max(system.network.in.bytes), kql='system.network.in.bytes: *') * 8",
timeScale: 's',
format: {
id: 'bits',
params: {
decimals: 1,
},
},
},
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return { [DEFAULT_LAYER_ID]: dataLayer };
};
getVisualizationState = (): XYState => {
return getXYVisualizationState({
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: ['y_network_in_bytes'],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
],
});
};
getFilters = (): Filter[] => {
return [
{
meta: {
disabled: false,
negate: false,
alias: null,
index: '3be1e71b-4bc5-4462-a314-04539f877a19',
key: 'system.network.in.bytes',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.network.in.bytes',
},
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
];
};
getReferences = (): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${DEFAULT_LAYER_ID}`,
},
];
};
}

View file

@ -0,0 +1,124 @@
/*
* 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 Filter, FilterStateStore } from '@kbn/es-query';
import type {
FormBasedLayer,
FormulaPublicApi,
PersistedIndexPatternLayer,
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,
getBreakdownColumn,
getHistogramColumn,
getXYVisualizationState,
} from '../utils';
import type { LensOptions } from '../../../../types';
import { ILensVisualization } from '../types';
const BREAKDOWN_COLUMN_NAME = 'hosts_aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export class TX implements ILensVisualization {
constructor(
private dataView: DataView,
private options: LensOptions,
private formula: FormulaPublicApi
) {}
getTitle(): string {
return 'Network Outbound (TX)';
}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers = (): Record<string, Omit<FormBasedLayer, 'indexPatternId'>> => {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...getBreakdownColumn(BREAKDOWN_COLUMN_NAME, 'host.name', this.options.breakdownSize),
...getHistogramColumn(HISTOGRAM_COLUMN_NAME, this.dataView.timeFieldName ?? '@timestamp'),
},
};
const dataLayer = this.formula.insertOrReplaceFormulaColumn(
'y_network_out_bytes',
{
formula:
"counter_rate(max(system.network.out.bytes), kql='system.network.out.bytes: *') * 8",
timeScale: 's',
format: {
id: 'bits',
params: {
decimals: 1,
},
},
},
baseLayer,
this.dataView
);
if (!dataLayer) {
throw new Error('Error generating the data layer for the chart');
}
return { [DEFAULT_LAYER_ID]: dataLayer };
};
getVisualizationState = (): XYState => {
return getXYVisualizationState({
layers: [
{
layerId: DEFAULT_LAYER_ID,
seriesType: 'line',
accessors: ['y_network_out_bytes'],
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
},
],
});
};
getFilters = (): Filter[] => {
return [
{
meta: {
disabled: false,
negate: false,
alias: null,
index: '3be1e71b-4bc5-4462-a314-04539f877a19',
key: 'system.network.out.bytes',
value: 'exists',
type: 'exists',
},
query: {
exists: {
field: 'system.network.out.bytes',
},
},
$state: {
store: FilterStateStore.APP_STATE,
},
},
];
};
getReferences = (): SavedObjectReference[] => {
return [
{
type: 'index-pattern',
id: this.dataView.id ?? '',
name: `indexpattern-datasource-layer-${DEFAULT_LAYER_ID}`,
},
];
};
}

View file

@ -0,0 +1,26 @@
/*
* 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 { LensAttributes } from '../../../types';
import { ILensVisualization } from './types';
export const buildLensAttributes = (visualization: ILensVisualization): LensAttributes => {
return {
title: visualization.getTitle(),
visualizationType: visualization.getVisualizationType(),
references: visualization.getReferences(),
state: {
datasourceStates: {
formBased: {
layers: visualization.getLayers(),
},
},
filters: visualization.getFilters(),
query: { language: 'kuery', query: '' },
visualization: visualization.getVisualizationState(),
},
};
};

View file

@ -0,0 +1,19 @@
/*
* 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 { SavedObjectReference } from '@kbn/core-saved-objects-common';
import { Filter } from '@kbn/es-query';
import { FormBasedLayer, XYState } from '@kbn/lens-plugin/public';
export interface ILensVisualization {
getTitle(): string;
getVisualizationType(): string;
getLayers(): Record<string, Omit<FormBasedLayer, 'indexPatternId'>>;
getVisualizationState(): XYState;
getFilters(): Filter[];
getReferences(): SavedObjectReference[];
}

View file

@ -0,0 +1,103 @@
/*
* 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 {
DateHistogramIndexPatternColumn,
PersistedIndexPatternLayer,
TermsIndexPatternColumn,
XYState,
} from '@kbn/lens-plugin/public';
export const DEFAULT_LAYER_ID = 'layer1';
export const getHistogramColumn = (columnName: string, sourceField: string) => {
return {
[columnName]: {
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: { interval: 'auto' },
scale: 'interval',
sourceField,
} as DateHistogramIndexPatternColumn,
};
};
export const getBreakdownColumn = (
columnName: string,
sourceField: string,
breakdownSize: number
): PersistedIndexPatternLayer['columns'] => {
return {
[columnName]: {
label: `Top ${breakdownSize} values of ${sourceField}`,
dataType: 'string',
operationType: 'terms',
scale: 'ordinal',
sourceField,
isBucketed: true,
params: {
size: breakdownSize,
orderBy: {
type: 'alphabetical',
fallback: false,
},
orderDirection: 'desc',
otherBucket: false,
missingBucket: false,
parentFormat: {
id: 'terms',
},
include: [],
exclude: [],
includeIsRegex: false,
excludeIsRegex: false,
},
} as TermsIndexPatternColumn,
};
};
export const getXYVisualizationState = (
custom: Omit<Partial<XYState>, 'layers'> & { layers: XYState['layers'] }
): XYState => ({
legend: {
isVisible: false,
position: 'right',
showSingleSeries: false,
},
valueLabels: 'show',
fittingFunction: 'None',
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,
yTitle: '',
xTitle: '',
hideEndzones: true,
...custom,
});

View file

@ -0,0 +1,229 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import { useLensAttributes } from './use_lens_attributes';
import type { DataView } from '@kbn/data-views-plugin/public';
import { coreMock } from '@kbn/core/public/mocks';
import { KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { InfraClientStartDeps } from '../types';
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
import { FilterStateStore } from '@kbn/es-query';
jest.mock('@kbn/kibana-react-plugin/public');
const useKibanaMock = useKibana as jest.MockedFunction<typeof useKibana>;
const mockDataView = {
id: 'mock-id',
title: 'mock-title',
timeFieldName: '@timestamp',
isPersisted: () => false,
getName: () => 'mock-data-view',
toSpec: () => ({}),
fields: [],
metaFields: [],
} as unknown as jest.Mocked<DataView>;
const lensPluginMockStart = lensPluginMock.createStartContract();
const mockUseKibana = () => {
useKibanaMock.mockReturnValue({
services: {
...coreMock.createStart(),
lens: { ...lensPluginMockStart },
} as Partial<CoreStart> & Partial<InfraClientStartDeps>,
} as unknown as KibanaReactContextValue<Partial<CoreStart> & Partial<InfraClientStartDeps>>);
};
describe('useHostTable hook', () => {
beforeEach(() => {
mockUseKibana();
});
it('should return the basic lens attributes', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLensAttributes({
type: 'load',
dataView: mockDataView,
})
);
await waitForNextUpdate();
const { state, title } = result.current.attributes ?? {};
const { datasourceStates, filters } = state ?? {};
expect(title).toBe('Normalized Load');
expect(datasourceStates).toEqual({
formBased: {
layers: {
layer1: {
columnOrder: ['hosts_aggs_breakdown', 'x_date_histogram', 'y_cpu_cores_usage'],
columns: {
hosts_aggs_breakdown: {
dataType: 'string',
isBucketed: true,
label: 'Top 10 values of host.name',
operationType: 'terms',
params: {
exclude: [],
excludeIsRegex: false,
include: [],
includeIsRegex: false,
missingBucket: false,
orderBy: {
fallback: false,
type: 'alphabetical',
},
orderDirection: 'desc',
otherBucket: false,
parentFormat: {
id: 'terms',
},
size: 10,
},
scale: 'ordinal',
sourceField: 'host.name',
},
x_date_histogram: {
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: {
interval: 'auto',
},
scale: 'interval',
sourceField: '@timestamp',
},
y_cpu_cores_usage: {
customLabel: false,
dataType: 'number',
filter: undefined,
isBucketed: false,
label: 'average(system.load.1) / max(system.load.cores)',
operationType: 'formula',
params: {
format: {
id: 'percent',
params: {
decimals: 0,
},
},
formula: 'average(system.load.1) / max(system.load.cores)',
isFormulaBroken: true,
},
reducedTimeRange: undefined,
references: [],
timeScale: undefined,
},
},
indexPatternId: 'mock-id',
},
referenceLayer: {
columnOrder: ['referenceColumn'],
columns: {
referenceColumn: {
customLabel: true,
dataType: 'number',
isBucketed: false,
isStaticValue: true,
label: 'Reference',
operationType: 'static_value',
params: {
format: {
id: 'percent',
params: {
decimals: 0,
},
},
value: 1,
},
references: [],
scale: 'ratio',
},
},
incompleteColumns: {},
linkToLayers: [],
sampling: 1,
},
},
},
});
expect(filters).toEqual([
{
$state: { store: 'appState' },
meta: {
alias: null,
disabled: false,
index: 'c1ec8212-ecee-494a-80da-f6f33b3393f2',
key: 'system.load.cores',
negate: false,
type: 'exists',
value: 'exists',
},
query: { exists: { field: 'system.load.cores' } },
},
{
$state: { store: 'appState' },
meta: {
alias: null,
disabled: false,
index: 'c1ec8212-ecee-494a-80da-f6f33b3393f2',
key: 'system.load.1',
negate: false,
type: 'exists',
value: 'exists',
},
query: { exists: { field: 'system.load.1' } },
},
]);
});
it('should return attributes with injected values', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLensAttributes({
type: 'load',
dataView: mockDataView,
})
);
await waitForNextUpdate();
const injectedData = {
query: {
language: 'kuery',
query: '{term: { host.name: "a"}}',
},
filters: [
{
$state: { store: FilterStateStore.APP_STATE },
meta: {
alias: null,
disabled: false,
index: 'c1ec8212-ecee-494a-80da-f6f33b3393f2',
key: 'system.load.cores',
negate: false,
type: 'range',
value: 'range',
},
query: { range: { 'system.load.cores': { gte: 0 } } },
},
],
title: 'Injected CPU Cores',
};
const injectedAttributes = result.current.injectData(injectedData);
const { state, title } = injectedAttributes ?? {};
const { filters, query } = state ?? {};
expect(title).toEqual(injectedData.title);
expect(query).toEqual(injectedData.query);
expect(filters).toHaveLength(3);
expect(filters).toContain(injectedData.filters[0]);
});
});

View file

@ -0,0 +1,115 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useMemo } from 'react';
import type { DataView } from '@kbn/data-views-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import { i18n } from '@kbn/i18n';
import useAsync from 'react-use/lib/useAsync';
import { InfraClientSetupDeps, LensAttributes, LensOptions } from '../types';
import {
buildLensAttributes,
HostLensAttributesTypes,
hostMetricsLensAttributes,
} from '../common/visualizations';
interface UseLensAttributesParams {
type: HostLensAttributesTypes;
dataView: DataView | undefined;
options?: LensOptions;
}
export const useLensAttributes = ({
type,
dataView,
options = {
breakdownSize: 10,
},
}: UseLensAttributesParams) => {
const {
services: { lens },
} = useKibana<InfraClientSetupDeps>();
const { navigateToPrefilledEditor } = lens;
const { value, error } = useAsync(lens.stateHelperApi, [lens]);
const { formula: formulaAPI } = value ?? {};
const attributes: LensAttributes | null = useMemo(() => {
if (!dataView || !formulaAPI) {
return null;
}
const VisualizationClass = hostMetricsLensAttributes[type];
const visualizationAttributes = buildLensAttributes(
new VisualizationClass(dataView, options, formulaAPI)
);
return visualizationAttributes;
}, [dataView, formulaAPI, options, type]);
const injectData = (data: {
filters: Filter[];
query: Query;
title?: string;
}): LensAttributes | null => {
if (!attributes) {
return null;
}
return {
...attributes,
...(!!data.title ? { title: data.title } : {}),
state: {
...attributes.state,
query: data.query,
filters: [...attributes.state.filters, ...data.filters],
},
};
};
const getExtraActions = (currentAttributes: LensAttributes | null, timeRange: TimeRange) => {
return {
openInLens: {
id: 'openInLens',
getDisplayName(_context: ActionExecutionContext): string {
return i18n.translate(
'xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines',
{
defaultMessage: 'Open in Lens',
}
);
},
getIconType(_context: ActionExecutionContext): string | undefined {
return 'visArea';
},
type: 'actionButton',
async isCompatible(_context: ActionExecutionContext): Promise<boolean> {
return true;
},
async execute(_context: ActionExecutionContext): Promise<void> {
if (currentAttributes) {
navigateToPrefilledEditor(
{
id: '',
timeRange,
attributes: currentAttributes,
},
{
openInNewTab: true,
}
);
}
},
order: 100,
},
};
};
return { attributes, injectData, getExtraActions, error };
};

View file

@ -16,10 +16,10 @@ export const ExperimentalBadge = () => (
display: flex;
justify-content: center;
`}
label={i18n.translate('xpack.infra.hostsPage.experimentalBadgeLabel', {
label={i18n.translate('xpack.infra.hostsViewPage.experimentalBadgeLabel', {
defaultMessage: 'Technical preview',
})}
tooltipContent={i18n.translate('xpack.infra.hostsPage.experimentalBadgeDescription', {
tooltipContent={i18n.translate('xpack.infra.hostsViewPage.experimentalBadgeDescription', {
defaultMessage:
'This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.',
})}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { InfraLoadingPanel } from '../../../../components/loading';
@ -13,7 +13,8 @@ import { useMetricsDataViewContext } from '../hooks/use_data_view';
import { UnifiedSearchBar } from './unified_search_bar';
import { HostsTable } from './hosts_table';
import { HostsViewProvider } from '../hooks/use_hosts_view';
import { MetricsTrend } from './metrics_trend/metrics_trend';
import { KPICharts } from './kpi_charts/kpi_charts';
import { Tabs } from './tabs/tabs';
export const HostContainer = () => {
const { metricsDataView, isDataViewLoading, hasFailedLoadingDataView } =
@ -36,9 +37,17 @@ export const HostContainer = () => {
<UnifiedSearchBar dataView={metricsDataView} />
<EuiSpacer />
<HostsViewProvider>
<MetricsTrend />
<EuiSpacer />
<HostsTable />
<EuiFlexGroup direction="column">
<EuiFlexItem>
<KPICharts />
</EuiFlexItem>
<EuiFlexItem>
<HostsTable />
</EuiFlexItem>
<EuiFlexItem>
<Tabs />
</EuiFlexItem>
</EuiFlexGroup>
</HostsViewProvider>
</>
);

View file

@ -87,7 +87,7 @@ export const HostsTable = () => {
if (loading) {
return (
<InfraLoadingPanel
height="100%"
height="185px"
width="auto"
text={i18n.translate('xpack.infra.waffle.loadingDataText', {
defaultMessage: 'Loading data',

View file

@ -48,7 +48,7 @@ export const buildHostsTableColumns = ({
return [
{
name: i18n.translate('xpack.infra.hostsTable.nameColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.nameColumnHeader', {
defaultMessage: 'Name',
}),
field: 'title',
@ -72,7 +72,7 @@ export const buildHostsTableColumns = ({
),
},
{
name: i18n.translate('xpack.infra.hostsTable.operatingSystemColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.operatingSystemColumnHeader', {
defaultMessage: 'Operating System',
}),
field: 'os',
@ -80,7 +80,7 @@ export const buildHostsTableColumns = ({
render: (os: string) => <EuiText size="s">{os}</EuiText>,
},
{
name: i18n.translate('xpack.infra.hostsTable.numberOfCpusColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.numberOfCpusColumnHeader', {
defaultMessage: '# of CPUs',
}),
field: 'cpuCores',
@ -91,7 +91,7 @@ export const buildHostsTableColumns = ({
align: 'right',
},
{
name: i18n.translate('xpack.infra.hostsTable.diskLatencyColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.diskLatencyColumnHeader', {
defaultMessage: 'Disk Latency (avg.)',
}),
field: 'diskLatency.avg',
@ -100,7 +100,7 @@ export const buildHostsTableColumns = ({
align: 'right',
},
{
name: i18n.translate('xpack.infra.hostsTable.averageTxColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.averageTxColumnHeader', {
defaultMessage: 'TX (avg.)',
}),
field: 'tx.avg',
@ -109,7 +109,7 @@ export const buildHostsTableColumns = ({
align: 'right',
},
{
name: i18n.translate('xpack.infra.hostsTable.averageRxColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.averageRxColumnHeader', {
defaultMessage: 'RX (avg.)',
}),
field: 'rx.avg',
@ -118,7 +118,7 @@ export const buildHostsTableColumns = ({
align: 'right',
},
{
name: i18n.translate('xpack.infra.hostsTable.averageMemoryTotalColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.averageMemoryTotalColumnHeader', {
defaultMessage: 'Memory total (avg.)',
}),
field: 'memoryTotal.avg',
@ -127,7 +127,7 @@ export const buildHostsTableColumns = ({
align: 'right',
},
{
name: i18n.translate('xpack.infra.hostsTable.averageMemoryUsageColumnHeader', {
name: i18n.translate('xpack.infra.hostsViewPage.table.averageMemoryUsageColumnHeader', {
defaultMessage: 'Memory usage (avg.)',
}),
field: 'memory.avg',

View file

@ -7,13 +7,13 @@
import React from 'react';
import { useHostsViewContext } from '../../hooks/use_hosts_view';
import { type ChartBaseProps, MetricsChart } from './metrics_chart';
import { type ChartBaseProps, KPIChart } from './kpi_chart';
export const HostsTile = ({ type, ...props }: ChartBaseProps) => {
const { hostViewState } = useHostsViewContext();
return (
<MetricsChart
<KPIChart
id={`$metric-${type}`}
type={type}
nodes={[]}

View file

@ -47,7 +47,7 @@ interface Props extends ChartBaseProps {
const MIN_HEIGHT = 150;
export const MetricsChart = ({
export const KPIChart = ({
color,
extra,
id,
@ -109,16 +109,16 @@ export const MetricsChart = ({
content={toolTip}
anchorClassName="eui-fullWidth"
>
<ChartStyled size={{ height: MIN_HEIGHT }}>
<KPIChartStyled size={{ height: MIN_HEIGHT }}>
<Metric id={id} data={[[metricsData]]} />
</ChartStyled>
</KPIChartStyled>
</EuiToolTip>
)}
</EuiPanel>
);
};
const ChartStyled = styled(Chart)`
const KPIChartStyled = styled(Chart)`
.echMetric {
border-radius: 5px;
}

View file

@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n';
import { Tile } from './tile';
import { HostsTile } from './hosts_tile';
export const MetricsTrend = () => {
export const KPICharts = () => {
return (
<EuiFlexGroup
direction="row"
@ -26,16 +26,16 @@ export const MetricsTrend = () => {
type="hostsCount"
metricType="value"
color="#6DCCB1"
title={i18n.translate('xpack.infra.hostsTable.metricTrend.hostCount.title', {
title={i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', {
defaultMessage: 'Hosts',
})}
trendA11yTitle={i18n.translate(
'xpack.infra.hostsTable.metricTrend.hostCount.a11y.title',
'xpack.infra.hostsViewPage.metricTrend.hostCount.a11y.title',
{
defaultMessage: 'CPU usage over time.',
}
)}
toolTip={i18n.translate('xpack.infra.hostsTable.metricTrend.hostCount.tooltip', {
toolTip={i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', {
defaultMessage: 'The number of hosts returned by your current search criteria.',
})}
data-test-subj="hostsView-metricsTrend-hosts"
@ -46,22 +46,22 @@ export const MetricsTrend = () => {
type="cpu"
metricType="avg"
color="#F1D86F"
title={i18n.translate('xpack.infra.hostsTable.metricTrend.cpu.title', {
title={i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.title', {
defaultMessage: 'CPU usage',
})}
subtitle={i18n.translate('xpack.infra.hostsTable.metricTrend.cpu.subtitle', {
subtitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.subtitle', {
defaultMessage: 'Average',
})}
trendA11yTitle={i18n.translate('xpack.infra.hostsTable.metricTrend.cpu.a11y.title', {
trendA11yTitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.a11y.title', {
defaultMessage: 'CPU usage over time.',
})}
trendA11yDescription={i18n.translate(
'xpack.infra.hostsTable.metricTrend.cpu.a11y.description',
'xpack.infra.hostsViewPage.metricTrend.cpu.a11y.description',
{
defaultMessage: 'A line chart showing the trend of the primary metric over time.',
}
)}
toolTip={i18n.translate('xpack.infra.hostsTable.metricTrend.cpu.tooltip', {
toolTip={i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.tooltip', {
defaultMessage:
'Average of percentage of CPU time spent in states other than Idle and IOWait, normalized by the number of CPU cores. Includes both time spent on user space and kernel space. 100% means all CPUs of the host are busy.',
})}
@ -73,22 +73,22 @@ export const MetricsTrend = () => {
type="memory"
metricType="avg"
color="#A987D1"
title={i18n.translate('xpack.infra.hostsTable.metricTrend.memory.title', {
title={i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.title', {
defaultMessage: 'Memory usage',
})}
subtitle={i18n.translate('xpack.infra.hostsTable.metricTrend.memory.subtitle', {
subtitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.subtitle', {
defaultMessage: 'Average',
})}
trendA11yTitle={i18n.translate('xpack.infra.hostsTable.metricTrend.memory.a11yTitle', {
trendA11yTitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.a11yTitle', {
defaultMessage: 'Memory usage over time.',
})}
trendA11yDescription={i18n.translate(
'xpack.infra.hostsTable.metricTrend.memory.a11yDescription',
'xpack.infra.hostsViewPage.metricTrend.memory.a11yDescription',
{
defaultMessage: 'A line chart showing the trend of the primary metric over time.',
}
)}
toolTip={i18n.translate('xpack.infra.hostsTable.metricTrend.memory.tooltip', {
toolTip={i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.tooltip', {
defaultMessage:
"Average of percentage of main memory usage excluding page cache. This includes resident memory for all processes plus memory used by the kernel structures and code apart the page cache. A high level indicates a situation of memory saturation for a host. 100% means the main memory is entirely filled with memory that can't be reclaimed, except by swapping out.",
})}
@ -100,22 +100,22 @@ export const MetricsTrend = () => {
type="rx"
metricType="avg"
color="#79AAD9"
title={i18n.translate('xpack.infra.hostsTable.metricTrend.rx.title', {
title={i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.title', {
defaultMessage: 'Network inbound (RX)',
})}
subtitle={i18n.translate('xpack.infra.hostsTable.metricTrend.rx.subtitle', {
subtitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.subtitle', {
defaultMessage: 'Average',
})}
trendA11yTitle={i18n.translate('xpack.infra.hostsTable.metricTrend.rx.a11y.title', {
trendA11yTitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.a11y.title', {
defaultMessage: 'Network inbound (RX) over time.',
})}
trendA11yDescription={i18n.translate(
'xpack.infra.hostsTable.metricTrend.rx.a11y.description',
'xpack.infra.hostsViewPage.metricTrend.rx.a11y.description',
{
defaultMessage: 'A line chart showing the trend of the primary metric over time.',
}
)}
toolTip={i18n.translate('xpack.infra.hostsTable.metricTrend.rx.tooltip', {
toolTip={i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.tooltip', {
defaultMessage:
'Number of bytes which have been received per second on the public interfaces of the hosts.',
})}
@ -127,22 +127,22 @@ export const MetricsTrend = () => {
type="tx"
metricType="avg"
color="#F5A35C"
title={i18n.translate('xpack.infra.hostsTable.metricTrend.tx.title', {
defaultMessage: 'Network outbound (TX) usage',
title={i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.title', {
defaultMessage: 'Network outbound (TX)',
})}
subtitle={i18n.translate('xpack.infra.hostsTable.metricTrend.tx.subtitle', {
subtitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.subtitle', {
defaultMessage: 'Average',
})}
trendA11yTitle={i18n.translate('xpack.infra.hostsTable.metricTrend.tx.a11.title', {
trendA11yTitle={i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.a11.title', {
defaultMessage: 'Network outbound (TX) usage over time.',
})}
trendA11yDescription={i18n.translate(
'xpack.infra.hostsTable.metricTrend.tx.a11y.description',
'xpack.infra.hostsViewPage.metricTrend.tx.a11y.description',
{
defaultMessage: 'A line chart showing the trend of the primary metric over time.',
}
)}
toolTip={i18n.translate('xpack.infra.hostsTable.metricTrend.tx.tooltip', {
toolTip={i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.tooltip', {
defaultMessage:
'Number of bytes which have been sent per second on the public interfaces of the hosts',
})}

View file

@ -9,7 +9,7 @@ import type { SnapshotMetricType } from '../../../../../../common/inventory_mode
import { useSnapshot } from '../../../inventory_view/hooks/use_snaphot';
import { useHostsViewContext } from '../../hooks/use_hosts_view';
import { type ChartBaseProps, MetricsChart } from './metrics_chart';
import { type ChartBaseProps, KPIChart } from './kpi_chart';
interface Props extends Omit<ChartBaseProps, 'type'> {
type: SnapshotMetricType;
@ -24,7 +24,5 @@ export const Tile = ({ type, ...props }: Props) => {
includeTimeseries: true,
});
return (
<MetricsChart id={`$metric-${type}`} type={type} nodes={nodes} loading={loading} {...props} />
);
return <KPIChart id={`$metric-${type}`} type={type} nodes={nodes} loading={loading} {...props} />;
};

View file

@ -0,0 +1,124 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Action } from '@kbn/ui-actions-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { BrushTriggerEvent } from '@kbn/charts-plugin/public';
import { EuiIcon, EuiPanel } from '@elastic/eui';
import { EuiFlexGroup } from '@elastic/eui';
import { EuiFlexItem } from '@elastic/eui';
import { EuiText } from '@elastic/eui';
import { EuiI18n } from '@elastic/eui';
import { InfraClientSetupDeps } from '../../../../../../types';
import { useLensAttributes } from '../../../../../../hooks/use_lens_attributes';
import { useMetricsDataViewContext } from '../../../hooks/use_data_view';
import { useUnifiedSearchContext } from '../../../hooks/use_unified_search';
import { HostLensAttributesTypes } from '../../../../../../common/visualizations';
export interface MetricChartProps {
title: string;
type: HostLensAttributesTypes;
breakdownSize: number;
}
const MIN_HEIGHT = 300;
export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => {
const {
unifiedSearchDateRange,
unifiedSearchQuery,
unifiedSearchFilters,
controlPanelFilters,
onSubmit,
} = useUnifiedSearchContext();
const { metricsDataView } = useMetricsDataViewContext();
const {
services: { lens },
} = useKibana<InfraClientSetupDeps>();
const EmbeddableComponent = lens.EmbeddableComponent;
const { injectData, getExtraActions, error } = useLensAttributes({
type,
dataView: metricsDataView,
options: {
breakdownSize,
},
});
const injectedLensAttributes = injectData({
filters: [...unifiedSearchFilters, ...controlPanelFilters],
query: unifiedSearchQuery,
title,
});
const extraActionOptions = getExtraActions(injectedLensAttributes, unifiedSearchDateRange);
const extraAction: Action[] = [extraActionOptions.openInLens];
const handleBrushEnd = ({ range }: BrushTriggerEvent['data']) => {
const [min, max] = range;
onSubmit({
dateRange: {
from: new Date(min).toISOString(),
to: new Date(max).toISOString(),
mode: 'absolute',
},
});
};
return (
<EuiPanel
borderRadius="m"
hasShadow={false}
hasBorder
paddingSize={error ? 'm' : 'none'}
style={{ minHeight: MIN_HEIGHT }}
data-test-subj={`hostsView-metricChart-${type}`}
>
{error ? (
<EuiFlexGroup
style={{ minHeight: MIN_HEIGHT, alignContent: 'center' }}
gutterSize="xs"
justifyContent="center"
alignItems="center"
direction="column"
>
<EuiFlexItem grow={false}>
<EuiIcon type="alert" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size="s" textAlign="center">
<EuiI18n
token="'xpack.infra.hostsViewPage.errorOnLoadingLensDependencies'"
default="There was an error trying to load Lens Plugin."
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
) : (
injectedLensAttributes && (
<EmbeddableComponent
id={`hostsViewsmetricsChart-${type}`}
style={{ height: MIN_HEIGHT }}
attributes={injectedLensAttributes}
viewMode={ViewMode.VIEW}
timeRange={unifiedSearchDateRange}
query={unifiedSearchQuery}
filters={unifiedSearchFilters}
extraActions={extraAction}
executionContext={{
type: 'infrastructure_observability_hosts_view',
name: `Hosts View ${type} Chart`,
}}
onBrushEnd={handleBrushEnd}
/>
)
)}
</EuiPanel>
);
};

View file

@ -0,0 +1,90 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFlexGrid, EuiFlexItem, EuiFlexGroup, EuiText, EuiI18n } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MetricChart, MetricChartProps } from './metric_chart';
const DEFAULT_BREAKDOWN_SIZE = 20;
const CHARTS_IN_ORDER: Array<Pick<MetricChartProps, 'title' | 'type'> & { fullRow?: boolean }> = [
{
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.load', {
defaultMessage: 'Normalized Load',
}),
type: 'load',
},
{
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.cpu', {
defaultMessage: 'CPU Usage',
}),
type: 'cpu',
},
{
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.memory', {
defaultMessage: 'Memory Usage',
}),
type: 'memory',
fullRow: true,
},
{
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.rx', {
defaultMessage: 'Network Inbound (RX)',
}),
type: 'rx',
},
{
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.tx', {
defaultMessage: 'Network Outbound (TX)',
}),
type: 'tx',
},
{
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.diskIORead', {
defaultMessage: 'Disk Read IOPS',
}),
type: 'diskIORead',
},
{
title: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.DiskIOWrite', {
defaultMessage: 'Disk Write IOPS',
}),
type: 'diskIOWrite',
},
];
export const MetricsGrid = () => {
return (
<EuiFlexGroup direction="column" gutterSize="s" data-test-subj="hostsView-metricChart">
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false} style={{ flex: 1 }}>
<EuiText size="xs">
<EuiI18n
token="xpack.infra.hostsViewPage.tabs.metricsCharts.sortingCriteria"
default="Showing for Top {maxHosts} hosts by {attribute}"
values={{
maxHosts: <strong>{DEFAULT_BREAKDOWN_SIZE}</strong>,
attribute: <strong>name</strong>,
}}
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGrid columns={2} gutterSize="s">
{CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => (
<EuiFlexItem style={fullRow ? { gridColumn: '1/-1' } : {}}>
<MetricChart breakdownSize={DEFAULT_BREAKDOWN_SIZE} {...chartProp} />
</EuiFlexItem>
))}
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiTabbedContent, EuiSpacer, type EuiTabbedContentTab } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MetricsGrid } from './metrics/metrics_grid';
interface WrapperProps {
children: React.ReactElement;
}
const Wrapper = ({ children }: WrapperProps) => {
return (
<>
<EuiSpacer size="s" />
{children}
</>
);
};
export const Tabs = () => {
const tabs: EuiTabbedContentTab[] = [
{
id: 'metrics',
name: i18n.translate('xpack.infra.hostsViewPage.tabs.metricsCharts.title', {
defaultMessage: 'Metrics',
}),
'data-test-subj': 'hostsView-tabs-metrics',
content: (
<Wrapper>
<MetricsGrid />
</Wrapper>
),
},
];
return <EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />;
};

View file

@ -23,7 +23,7 @@ interface Props {
export const UnifiedSearchBar = ({ dataView }: Props) => {
const {
services: { unifiedSearch },
services: { unifiedSearch, application },
} = useKibana<InfraClientStartDeps>();
const {
unifiedSearchDateRange,
@ -37,10 +37,6 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
const { SearchBar } = unifiedSearch.ui;
const onFilterChange = (filters: Filter[]) => {
onQueryChange({ filters });
};
const onQuerySubmit = (payload: { dateRange: TimeRange; query?: Query }) => {
onQueryChange({ payload });
};
@ -62,14 +58,12 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
const onQueryChange = ({
payload,
filters,
panelFilters,
}: {
payload?: { dateRange: TimeRange; query?: Query };
filters?: Filter[];
panelFilters?: Filter[];
}) => {
onSubmit({ query: payload?.query, dateRange: payload?.dateRange, filters, panelFilters });
onSubmit({ query: payload?.query, dateRange: payload?.dateRange, panelFilters });
};
return (
@ -83,14 +77,12 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
query={unifiedSearchQuery}
dateRangeFrom={unifiedSearchDateRange.from}
dateRangeTo={unifiedSearchDateRange.to}
filters={unifiedSearchFilters}
onQuerySubmit={onQuerySubmit}
onSaved={onQuerySave}
onSavedQueryUpdated={onQuerySave}
onClearSavedQuery={onClearSavedQuery}
showSaveQuery
showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)}
showQueryInput
onFiltersUpdated={onFilterChange}
displayStyle="inPage"
/>
<ControlsContent

View file

@ -9,8 +9,7 @@ import { useDataView } from './use_data_view';
import { renderHook } from '@testing-library/react-hooks';
import { KibanaReactContextValue, useKibana } from '@kbn/kibana-react-plugin/public';
import { coreMock, notificationServiceMock } from '@kbn/core/public/mocks';
import type { DataView } from '@kbn/data-views-plugin/public';
import { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
import type { DataView, DataViewsServicePublic } from '@kbn/data-views-plugin/public';
import { InfraClientStartDeps } from '../../../../types';
import { CoreStart } from '@kbn/core/public';

View file

@ -76,7 +76,7 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => {
useEffect(() => {
if (hasFailedLoadingDataView && notifications) {
notifications.toasts.addDanger(
i18n.translate('xpack.infra.hostsTable.errorOnCreateOrLoadDataview', {
i18n.translate('xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview', {
defaultMessage:
'There was an error trying to load or create the Data View: {metricAlias}',
values: { metricAlias },

View file

@ -6,14 +6,15 @@
*/
import { useKibana } from '@kbn/kibana-react-plugin/public';
import createContainer from 'constate';
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { buildEsQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { debounce } from 'lodash';
import type { SavedQuery } from '@kbn/data-plugin/public';
import { debounce } from 'lodash';
import deepEqual from 'fast-deep-equal';
import type { InfraClientStartDeps } from '../../../../types';
import { useMetricsDataViewContext } from './use_data_view';
import { useSyncKibanaTimeFilterTime } from '../../../../hooks/use_kibana_timefilter_time';
import { useHostsUrlState, INITIAL_DATE_RANGE } from './use_hosts_url_state';
import { useHostsUrlState, INITIAL_DATE_RANGE } from './use_unified_search_url_state';
export const useUnifiedSearch = () => {
const { state, dispatch, getRangeInTimestamp, getTime } = useHostsUrlState();
@ -30,6 +31,37 @@ export const useUnifiedSearch = () => {
const { filterManager } = queryManager;
useEffect(() => {
const { filters } = state;
if (!deepEqual(filters, filterManager.getFilters())) {
filterManager.setFilters(filters);
}
}, [filterManager, state]);
// This will listen and react to all changes in filterManager and timefilter values,
// to allow other components in the page to communicate with the unified search
useEffect(() => {
const next = () => {
const globalFilters = filterManager.getFilters();
debounceOnSubmit({
filters: globalFilters,
dateRange: getTime(),
});
};
const filterSubscription = filterManager.getUpdates$().subscribe({
next,
});
const timeSubscription = queryManager.timefilter.timefilter.getTimeUpdate$().subscribe({
next,
});
return () => {
filterSubscription.unsubscribe();
timeSubscription.unsubscribe();
};
});
const onSubmit = useCallback(
(data?: {
query?: Query;
@ -40,22 +72,18 @@ export const useUnifiedSearch = () => {
const { query, dateRange, filters, panelFilters } = data ?? {};
const newDateRange = dateRange ?? getTime();
if (filters) {
filterManager.setFilters(filters);
}
dispatch({
type: 'setQuery',
payload: {
query,
filters: filters ? filterManager.getFilters() : undefined,
filters,
dateRange: newDateRange,
dateRangeTimestamp: getRangeInTimestamp(newDateRange),
panelFilters,
},
});
},
[getTime, dispatch, filterManager, getRangeInTimestamp]
[getTime, dispatch, getRangeInTimestamp]
);
// This won't prevent onSubmit from being fired twice when `clear filters` is clicked,
@ -107,7 +135,7 @@ export const useUnifiedSearch = () => {
onSubmit: debounceOnSubmit,
saveQuery,
unifiedSearchQuery: state.query,
unifiedSearchDateRange: getTime(),
unifiedSearchDateRange: state.dateRange,
unifiedSearchFilters: state.filters,
};
};

View file

@ -73,8 +73,9 @@ const reducer = (state: HostsState, action: Action): HostsState => {
};
export const useHostsUrlState = () => {
const [getTime] = useKibanaTimefilterTime(INITIAL_DATE_RANGE);
const [urlState, setUrlState] = useUrlState<HostsState>({
defaultState: INITIAL_HOSTS_STATE,
defaultState: { ...INITIAL_HOSTS_STATE, dateRange: getTime() },
decodeUrlState,
encodeUrlState,
urlStateKey: '_a',
@ -83,8 +84,6 @@ export const useHostsUrlState = () => {
const [state, dispatch] = useReducer(reducer, urlState);
const [getTime] = useKibanaTimefilterTime(INITIAL_DATE_RANGE);
const getRangeInTimestamp = useCallback(({ from, to }: TimeRange) => {
const fromTS = DateMath.parse(from)?.valueOf() ?? CALCULATED_DATE_RANGE_FROM;
const toTS = DateMath.parse(to)?.valueOf() ?? CALCULATED_DATE_RANGE_TO;

View file

@ -28,8 +28,9 @@ import type {
} from '@kbn/observability-plugin/public';
// import type { OsqueryPluginStart } from '../../osquery/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { UnwrapPromise } from '../common/utility_types';
import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { type TypedLensByValueInput, LensPublicStart } from '@kbn/lens-plugin/public';
import type { UnwrapPromise } from '../common/utility_types';
import type {
SourceProviderProps,
UseNodeMetricsTableOptions,
@ -62,6 +63,7 @@ export interface InfraClientSetupDeps {
ml: MlPluginSetup;
embeddable: EmbeddableSetup;
share: SharePluginSetup;
lens: LensPublicStart;
}
export interface InfraClientStartDeps {
@ -77,6 +79,7 @@ export interface InfraClientStartDeps {
osquery?: unknown; // OsqueryPluginStart;
share: SharePluginStart;
storage: IStorageWrapper;
lens: LensPublicStart;
}
export type InfraClientCoreSetup = CoreSetup<InfraClientStartDeps, InfraClientStartExports>;
@ -96,3 +99,9 @@ export interface InfraHttpError extends IHttpFetchError {
message?: string;
};
}
export type LensAttributes = TypedLensByValueInput['attributes'];
export interface LensOptions {
breakdownSize: number;
}

View file

@ -1,15 +1,9 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"outDir": "target/types"
},
"include": [
"../../../typings/**/*",
"common/**/*",
"public/**/*",
"server/**/*",
"types/**/*"
],
"include": ["../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*", "types/**/*"],
"kbn_references": [
"@kbn/core",
"@kbn/data-plugin",
@ -53,10 +47,12 @@
"@kbn/logging-mocks",
"@kbn/field-types",
"@kbn/es-types",
"@kbn/ui-actions-plugin",
"@kbn/charts-plugin",
"@kbn/lens-plugin",
"@kbn/core-saved-objects-common",
"@kbn/core-analytics-server",
"@kbn/analytics-client",
"@kbn/analytics-client"
],
"exclude": [
"target/**/*",
]
"exclude": ["target/**/*"]
}

View file

@ -38,4 +38,4 @@ metricbeat.modules:
output.elasticsearch:
hosts: [ "host.docker.internal:9200" ]
username: "elastic"
password: "changeme"
password: "changeme"

View file

@ -16290,7 +16290,6 @@
"xpack.infra.deprecations.tiebreakerAdjustIndexing": "Ajustez votre indexation pour utiliser \"{field}\" comme moyen de départager.",
"xpack.infra.deprecations.timestampAdjustIndexing": "Ajustez votre indexation pour utiliser \"{field}\" comme horodatage.",
"xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "Dernières {duration} de données pour l'heure sélectionnée",
"xpack.infra.hostsTable.errorOnCreateOrLoadDataview": "Une erreur s'est produite lors du chargement ou de la création de la vue de données : {metricAlias}",
"xpack.infra.inventoryTimeline.header": "{metricLabel} moyen",
"xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "Le modèle de {metricId} nécessite un cloudId, mais aucun n'a été attribué à {nodeId}.",
"xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} n'est pas une valeur inframétrique valide",
@ -16498,16 +16497,27 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "Voir les instructions de configuration",
"xpack.infra.homePage.settingsTabTitle": "Paramètres",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "Rechercher des données d'infrastructure… (par exemple host.name:host-1)",
"xpack.infra.hostsPage.experimentalBadgeDescription": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera au maximum de corriger tout problème, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale.",
"xpack.infra.hostsPage.experimentalBadgeLabel": "Version d'évaluation technique",
"xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "Total de la mémoire (moy.)",
"xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "Utilisation de la mémoire (moy.)",
"xpack.infra.hostsTable.averageRxColumnHeader": "RX (moy.)",
"xpack.infra.hostsTable.averageTxColumnHeader": "TX (moy.)",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "Latence du disque (moy.)",
"xpack.infra.hostsTable.nameColumnHeader": "Nom",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "Nombre de processeurs",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "Système d'exploitation",
"xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview": "Une erreur s'est produite lors du chargement ou de la création de la vue de données : {metricAlias}",
"xpack.infra.hostsViewPage.experimentalBadgeDescription": "Cette fonctionnalité est en version d'évaluation technique et pourra être modifiée ou retirée complètement dans une future version. Elastic s'efforcera au maximum de corriger tout problème, mais les fonctionnalités en version d'évaluation technique ne sont pas soumises aux accords de niveau de service d'assistance des fonctionnalités officielles en disponibilité générale.",
"xpack.infra.hostsViewPage.experimentalBadgeLabel": "Version d'évaluation technique",
"xpack.infra.hostsViewPage.metricTrend.cpu.title": "Utilisation CPU",
"xpack.infra.hostsViewPage.metricTrend.memory.title": "Utilisation mémoire",
"xpack.infra.hostsViewPage.metricTrend.rx.title": "Entrant (RX)",
"xpack.infra.hostsViewPage.metricTrend.tx.title": "Sortant (TX)",
"xpack.infra.hostsViewPage.table.averageMemoryTotalColumnHeader": "Total de la mémoire (moy.)",
"xpack.infra.hostsViewPage.table.averageMemoryUsageColumnHeader": "Utilisation de la mémoire (moy.)",
"xpack.infra.hostsViewPage.table.averageRxColumnHeader": "RX (moy.)",
"xpack.infra.hostsViewPage.table.averageTxColumnHeader": "TX (moy.)",
"xpack.infra.hostsViewPage.table.diskLatencyColumnHeader": "Latence du disque (moy.)",
"xpack.infra.hostsViewPage.table.nameColumnHeader": "Nom",
"xpack.infra.hostsViewPage.table.numberOfCpusColumnHeader": "Nombre de processeurs",
"xpack.infra.hostsViewPage.table.operatingSystemColumnHeader": "Système d'exploitation",
"xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "Ouvrir dans Lens",
"xpack.infra.hostsViewPage.tabs.metricsCharts.title": "Indicateurs",
"xpack.infra.hostsViewPage.tabs.metricsCharts.cpu": "Utilisation CPU",
"xpack.infra.hostsViewPage.tabs.metricsCharts.memory": "Utilisation mémoire",
"xpack.infra.hostsViewPage.tabs.metricsCharts.rx": "Entrant (RX)",
"xpack.infra.hostsViewPage.tabs.metricsCharts.tx": "Sortant (TX)",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "Créer une règle d'inventaire",
"xpack.infra.infra.nodeDetails.openAsPage": "Ouvrir en tant que page",
@ -36581,4 +36591,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "Présentation"
}
}
}

View file

@ -16277,7 +16277,6 @@
"xpack.infra.deprecations.tiebreakerAdjustIndexing": "インデックスを調整し、\"{field}\"をタイブレーカーとして使用します。",
"xpack.infra.deprecations.timestampAdjustIndexing": "インデックスを調整し、\"{field}\"をタイムスタンプとして使用します。",
"xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "指定期間のデータの最後の{duration}",
"xpack.infra.hostsTable.errorOnCreateOrLoadDataview": "データビューの読み込みまたは作成中にエラーが発生しました:{metricAlias}",
"xpack.infra.inventoryTimeline.header": "平均{metricLabel}",
"xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} のモデルには cloudId が必要ですが、{nodeId} に cloudId が指定されていません。",
"xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} は有効な InfraMetric ではありません",
@ -16484,16 +16483,26 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "セットアップの手順を表示",
"xpack.infra.homePage.settingsTabTitle": "設定",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "インフラストラクチャデータを検索…host.name:host-1",
"xpack.infra.hostsPage.experimentalBadgeDescription": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticは最善の努力を講じてすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。",
"xpack.infra.hostsPage.experimentalBadgeLabel": "テクニカルプレビュー",
"xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "メモリー合計(平均)",
"xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "メモリー使用状況(平均)",
"xpack.infra.hostsTable.averageRxColumnHeader": "RX平均",
"xpack.infra.hostsTable.averageTxColumnHeader": "TX平均",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "ディスクレイテンシ(平均)",
"xpack.infra.hostsTable.nameColumnHeader": "名前",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "CPU数",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "オペレーティングシステム",
"xpack.infra.hostsViewPage.experimentalBadgeDescription": "この機能はテクニカルプレビュー中であり、将来のリリースでは変更されたり完全に削除されたりする場合があります。Elasticは最善の努力を講じてすべての問題の修正に努めますが、テクニカルプレビュー中の機能には正式なGA機能のサポートSLAが適用されません。",
"xpack.infra.hostsViewPage.experimentalBadgeLabel": "テクニカルプレビュー",
"xpack.infra.hostsViewPage.metricTrend.cpu.title": "CPU使用状況",
"xpack.infra.hostsViewPage.metricTrend.memory.title": "メモリー使用状況",
"xpack.infra.hostsViewPage.metricTrend.rx.title": "受信RX)",
"xpack.infra.hostsViewPage.metricTrend.tx.title": "送信TX)",
"xpack.infra.hostsViewPage.table.averageMemoryTotalColumnHeader": "メモリー合計(平均)",
"xpack.infra.hostsViewPage.table.averageMemoryUsageColumnHeader": "メモリー使用状況(平均)",
"xpack.infra.hostsViewPage.table.averageRxColumnHeader": "RX平均",
"xpack.infra.hostsViewPage.table.averageTxColumnHeader": "TX平均",
"xpack.infra.hostsViewPage.table.diskLatencyColumnHeader": "ディスクレイテンシ(平均)",
"xpack.infra.hostsViewPage.table.nameColumnHeader": "名前",
"xpack.infra.hostsViewPage.table.numberOfCpusColumnHeader": "CPU数",
"xpack.infra.hostsViewPage.table.operatingSystemColumnHeader": "オペレーティングシステム",
"xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "Lensで開く",
"xpack.infra.hostsViewPage.tabs.metricsCharts.title": "メトリック",
"xpack.infra.hostsViewPage.tabs.metricsCharts.cpu": "CPU使用状況",
"xpack.infra.hostsViewPage.tabs.metricsCharts.memory": "メモリー使用状況",
"xpack.infra.hostsViewPage.tabs.metricsCharts.rx": "受信RX)",
"xpack.infra.hostsViewPage.tabs.metricsCharts.tx": "送信TX)",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "インベントリルールの作成",
"xpack.infra.infra.nodeDetails.openAsPage": "ページとして開く",
@ -36550,4 +36559,4 @@
"xpack.painlessLab.title": "Painless Lab",
"xpack.painlessLab.walkthroughButtonLabel": "実地検証"
}
}
}

View file

@ -16295,7 +16295,6 @@
"xpack.infra.deprecations.tiebreakerAdjustIndexing": "调整索引以将“{field}”用作决胜属性。",
"xpack.infra.deprecations.timestampAdjustIndexing": "调整索引以将“{field}”用作时间戳。",
"xpack.infra.homePage.toolbar.showingLastOneMinuteDataText": "选定时间过去 {duration}的数据",
"xpack.infra.hostsTable.errorOnCreateOrLoadDataview": "尝试加载或创建以下数据视图时出错:{metricAlias}",
"xpack.infra.inventoryTimeline.header": "平均值 {metricLabel}",
"xpack.infra.kibanaMetrics.cloudIdMissingErrorMessage": "{metricId} 的模型需要云 ID但没有为 {nodeId} 提供。",
"xpack.infra.kibanaMetrics.invalidInfraMetricErrorMessage": "{id} 不是有效的 InfraMetric",
@ -16503,16 +16502,27 @@
"xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel": "查看设置说明",
"xpack.infra.homePage.settingsTabTitle": "设置",
"xpack.infra.homePage.toolbar.kqlSearchFieldPlaceholder": "搜索基础设施数据……(例如 host.name:host-1",
"xpack.infra.hostsPage.experimentalBadgeDescription": "此功能处于技术预览状态在未来版本中可能会更改或完全移除。Elastic 将尽最大努力来修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。",
"xpack.infra.hostsPage.experimentalBadgeLabel": "技术预览",
"xpack.infra.hostsTable.averageMemoryTotalColumnHeader": "内存合计(平均值)",
"xpack.infra.hostsTable.averageMemoryUsageColumnHeader": "内存使用率(平均值)",
"xpack.infra.hostsTable.averageRxColumnHeader": "RX平均值",
"xpack.infra.hostsTable.averageTxColumnHeader": "TX平均值",
"xpack.infra.hostsTable.diskLatencyColumnHeader": "磁盘延迟(平均值)",
"xpack.infra.hostsTable.nameColumnHeader": "名称",
"xpack.infra.hostsTable.numberOfCpusColumnHeader": "# 个 CPU",
"xpack.infra.hostsTable.operatingSystemColumnHeader": "操作系统",
"xpack.infra.hostsViewPage.errorOnCreateOrLoadDataview": "尝试加载或创建以下数据视图时出错:{metricAlias}",
"xpack.infra.hostsViewPage.experimentalBadgeDescription": "此功能处于技术预览状态在未来版本中可能会更改或完全移除。Elastic 将尽最大努力来修复任何问题,但处于技术预览状态的功能不受正式 GA 功能支持 SLA 的约束。",
"xpack.infra.hostsViewPage.experimentalBadgeLabel": "技术预览",
"xpack.infra.hostsViewPage.metricTrend.cpu.title": "CPU 使用率",
"xpack.infra.hostsViewPage.metricTrend.memory.title": "内存利用率",
"xpack.infra.hostsViewPage.metricTrend.rx.title": "入站 (RX)",
"xpack.infra.hostsViewPage.metricTrend.tx.title": "出站 (TX)",
"xpack.infra.hostsViewPage.table.averageMemoryTotalColumnHeader": "内存合计(平均值)",
"xpack.infra.hostsViewPage.table.averageMemoryUsageColumnHeader": "内存使用率(平均值)",
"xpack.infra.hostsViewPage.table.averageRxColumnHeader": "RX平均值",
"xpack.infra.hostsViewPage.table.averageTxColumnHeader": "TX平均值",
"xpack.infra.hostsViewPage.table.diskLatencyColumnHeader": "磁盘延迟(平均值)",
"xpack.infra.hostsViewPage.table.nameColumnHeader": "名称",
"xpack.infra.hostsViewPage.table.numberOfCpusColumnHeader": "# 个 CPU",
"xpack.infra.hostsViewPage.table.operatingSystemColumnHeader": "操作系统",
"xpack.infra.hostsViewPage.tabs.metricsCharts.actions.openInLines": "在 Lens 中打开",
"xpack.infra.hostsViewPage.tabs.metricsCharts.title": "指标",
"xpack.infra.hostsViewPage.tabs.metricsCharts.cpu": "CPU 使用率",
"xpack.infra.hostsViewPage.tabs.metricsCharts.memory": "内存利用率",
"xpack.infra.hostsViewPage.tabs.metricsCharts.rx": "入站 (RX)",
"xpack.infra.hostsViewPage.tabs.metricsCharts.tx": "出站 (TX)",
"xpack.infra.infra.nodeDetails.apmTabLabel": "APM",
"xpack.infra.infra.nodeDetails.createAlertLink": "创建库存规则",
"xpack.infra.infra.nodeDetails.openAsPage": "以页面形式打开",
@ -36586,4 +36596,4 @@
"xpack.painlessLab.title": "Painless 实验室",
"xpack.painlessLab.walkthroughButtonLabel": "指导"
}
}
}

View file

@ -49,7 +49,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
});
it('should load 5 metrics trend tiles', async () => {
const hosts = await pageObjects.infraHostsView.getMetricsTrendTilesCount();
const hosts = await pageObjects.infraHostsView.getAllMetricsTrendTiles();
expect(hosts.length).to.equal(5);
});
@ -65,6 +65,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(tileValue).to.eql(value);
});
});
describe('Lens charts', () => {
it('should load 7 lens metric charts', async () => {
const metricCharts = await pageObjects.infraHostsView.getAllMetricsCharts();
expect(metricCharts.length).to.equal(7);
});
it('should have an option to open the chart in lens', async () => {
await pageObjects.infraHostsView.getOpenInLensOption();
});
});
});
});
};

View file

@ -20,7 +20,11 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
return testSubjects.find('hostsView-metricsTrend');
},
async getMetricsTrendTilesCount() {
async getChartsContainer() {
return testSubjects.find('hostsView-metricChart');
},
async getAllMetricsTrendTiles() {
const container = await this.getMetricsTrendContainer();
return container.findAllByCssSelector('[data-test-subj*="hostsView-metricsTrend-"]');
},
@ -31,5 +35,20 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
const div = await element.findByClassName('echMetricText__value');
return await div.getAttribute('title');
},
async getAllMetricsCharts() {
const container = await this.getChartsContainer();
return container.findAllByCssSelector('[data-test-subj*="hostsView-metricChart-"]');
},
async getOpenInLensOption() {
const metricCharts = await this.getAllMetricsCharts();
const chart = metricCharts[0];
await chart.moveMouseTo();
const button = await testSubjects.findDescendant('embeddablePanelToggleMenuIcon', chart);
await button.click();
await testSubjects.existOrFail('embeddablePanelContextMenuOpen');
return testSubjects.existOrFail('embeddablePanelAction-openInLens');
},
};
}