Merge branch 'main' into kibana_context

This commit is contained in:
Clint Andrew Hall 2023-07-10 17:02:58 +02:00 committed by GitHub
commit 07103a02dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 1971 additions and 1249 deletions

View file

@ -18,8 +18,8 @@ import {
EuiFlexItem,
EuiFormRow,
EuiHorizontalRule,
EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageSection,
EuiPageHeader,
EuiSelect,
EuiSpacer,
EuiText,
@ -149,7 +149,7 @@ export const Main = (props: MainProps) => {
return (
<>
<EuiPageContentHeader>
<EuiPageHeader>
<EuiTitle>
<h2>
<FormattedMessage
@ -158,8 +158,8 @@ export const Main = (props: MainProps) => {
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
</EuiPageHeader>
<EuiPageSection>
<EuiText>
<h3>
<FormattedMessage
@ -354,7 +354,7 @@ export const Main = (props: MainProps) => {
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageSection>
</>
);
};

View file

@ -12,11 +12,7 @@ import { EuiButton, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiCode,
} from '@elastic/eui';
import { EuiPageHeader, EuiPageSection, EuiCode } from '@elastic/eui';
import { useParams } from 'react-router-dom';
interface StepFourProps {
@ -41,7 +37,7 @@ export const StepFour: React.FC<StepFourProps> = ({
return (
<>
<EuiPageContentHeader>
<EuiPageHeader>
<EuiTitle>
<h2>
<FormattedMessage
@ -50,8 +46,8 @@ export const StepFour: React.FC<StepFourProps> = ({
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
</EuiPageHeader>
<EuiPageSection>
<EuiText>
<p>
<FormattedMessage
@ -74,7 +70,7 @@ export const StepFour: React.FC<StepFourProps> = ({
>
Complete step 4
</EuiButton>
</EuiPageContentBody>
</EuiPageSection>
</>
);
};

View file

@ -13,8 +13,8 @@ import {
EuiText,
EuiTourStep,
EuiTitle,
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
EuiPageHeader,
EuiPageSection,
EuiSpacer,
EuiCode,
EuiFieldText,
@ -46,7 +46,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
}, [isTourActive]);
return (
<>
<EuiPageContentHeader>
<EuiPageHeader>
<EuiTitle>
<h2>
<FormattedMessage
@ -55,8 +55,8 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
</EuiPageHeader>
<EuiPageSection>
<EuiText>
<p>
<FormattedMessage
@ -118,7 +118,7 @@ export const StepOne = ({ guidedOnboarding }: GuidedOnboardingExampleAppDeps) =>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPageContentBody>
</EuiPageSection>
</>
);
};

View file

@ -12,10 +12,7 @@ import { EuiButton, EuiSpacer, EuiText, EuiTitle, EuiTourStep } from '@elastic/e
import { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public/types';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
} from '@elastic/eui';
import { EuiPageHeader, EuiPageSection } from '@elastic/eui';
interface StepThreeProps {
guidedOnboarding: GuidedOnboardingPluginStart;
@ -39,7 +36,7 @@ export const StepThree = (props: StepThreeProps) => {
return (
<>
<EuiPageContentHeader>
<EuiPageHeader>
<EuiTitle>
<h2>
<FormattedMessage
@ -48,8 +45,8 @@ export const StepThree = (props: StepThreeProps) => {
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
</EuiPageHeader>
<EuiPageSection>
<EuiText>
<p>
<FormattedMessage
@ -92,7 +89,7 @@ export const StepThree = (props: StepThreeProps) => {
Complete step 3
</EuiButton>
</EuiTourStep>
</EuiPageContentBody>
</EuiPageSection>
</>
);
};

View file

@ -11,15 +11,12 @@ import React from 'react';
import { EuiText, EuiTitle } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentBody_Deprecated as EuiPageContentBody,
} from '@elastic/eui';
import { EuiPageHeader, EuiPageSection } from '@elastic/eui';
export const StepTwo = () => {
return (
<>
<EuiPageContentHeader>
<EuiPageHeader>
<EuiTitle>
<h2>
<FormattedMessage
@ -28,8 +25,8 @@ export const StepTwo = () => {
/>
</h2>
</EuiTitle>
</EuiPageContentHeader>
<EuiPageContentBody>
</EuiPageHeader>
<EuiPageSection>
<EuiText>
<p>
<FormattedMessage
@ -39,7 +36,7 @@ export const StepTwo = () => {
/>
</p>
</EuiText>
</EuiPageContentBody>
</EuiPageSection>
</>
);
};

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { DataView } from '@kbn/data-views-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
export const fields = [
export const shallowMockedFields = [
{
name: '_source',
type: '_source',
@ -73,6 +73,10 @@ export const fields = [
},
] as DataView['fields'];
export const deepMockedFields = shallowMockedFields.map(
(field) => new DataViewField(field)
) as DataView['fields'];
export const buildDataViewMock = ({
name,
fields: definedFields,
@ -120,4 +124,7 @@ export const buildDataViewMock = ({
return dataView;
};
export const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields });
export const dataViewMock = buildDataViewMock({
name: 'the-data-view',
fields: shallowMockedFields,
});

View file

@ -11,7 +11,7 @@ import { EuiCopy } from '@elastic/eui';
import { act } from 'react-dom/test-utils';
import { findTestSubject } from '@elastic/eui/lib/test';
import { esHits } from '../../__mocks__/es_hits';
import { buildDataViewMock, fields } from '../../__mocks__/data_view';
import { buildDataViewMock, deepMockedFields } from '../../__mocks__/data_view';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { DiscoverGrid, DiscoverGridProps } from './discover_grid';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
@ -28,7 +28,7 @@ jest.mock('@kbn/cell-actions', () => ({
export const dataViewMock = buildDataViewMock({
name: 'the-data-view',
fields,
fields: deepMockedFields,
timeFieldName: '@timestamp',
});
@ -259,18 +259,8 @@ describe('DiscoverGrid', () => {
triggerId: 'test',
getCellValue: expect.any(Function),
fields: [
{
name: '@timestamp',
type: 'date',
aggregatable: true,
searchable: undefined,
},
{
name: 'message',
type: 'string',
aggregatable: false,
searchable: undefined,
},
dataViewMock.getFieldByName('@timestamp')?.toSpec(),
dataViewMock.getFieldByName('message')?.toSpec(),
],
})
);

View file

@ -456,23 +456,15 @@ export const DiscoverGrid = ({
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
() =>
cellActionsTriggerId && !isPlainRecord
? visibleColumns.map((columnName) => {
const field = dataView.getFieldByName(columnName);
if (!field) {
return {
? visibleColumns.map(
(columnName) =>
dataView.getFieldByName(columnName)?.toSpec() ?? {
name: '',
type: '',
aggregatable: false,
searchable: false,
};
}
return {
name: columnName,
type: field.type,
aggregatable: field.aggregatable,
searchable: field.searchable,
};
})
}
)
: undefined,
[cellActionsTriggerId, isPlainRecord, visibleColumns, dataView]
);

View file

@ -12,7 +12,6 @@ import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_ma
import { SearchInput } from '..';
import { getSavedSearchUrl } from '@kbn/saved-search-plugin/public';
import { DiscoverServices } from '../build_services';
import { dataViewMock } from '../__mocks__/data_view';
import { discoverServiceMock } from '../__mocks__/services';
import { SavedSearchEmbeddable, SearchEmbeddableConfig } from './saved_search_embeddable';
import { render } from 'react-dom';
@ -23,6 +22,7 @@ import { SHOW_FIELD_STATISTICS } from '../../common';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { SavedSearchEmbeddableComponent } from './saved_search_embeddable_component';
import { VIEW_MODE } from '../../common/constants';
import { buildDataViewMock, deepMockedFields } from '../__mocks__/data_view';
let discoverComponent: ReactWrapper;
@ -48,6 +48,8 @@ function getSearchResponse(nrOfHits: number) {
});
}
const dataViewMock = buildDataViewMock({ name: 'the-data-view', fields: deepMockedFields });
describe('saved search embeddable', () => {
let mountpoint: HTMLDivElement;
let filterManagerMock: jest.Mocked<FilterManager>;

View file

@ -84,6 +84,7 @@ export const DataTable = () => {
<StoryProviders>
<DataTableComponent
browserFields={{}}
getFieldSpec={() => undefined}
data={mockTimelineData}
id={TableId.test}
renderCellValue={StoryCellRenderer}

View file

@ -71,6 +71,7 @@ describe('DataTable', () => {
const mount = useMountAppended();
const props: DataTableProps = {
browserFields: mockBrowserFields,
getFieldSpec: () => undefined,
data: mockTimelineData,
id: TableId.test,
loadPage: jest.fn(),
@ -158,11 +159,21 @@ describe('DataTable', () => {
describe('cellActions', () => {
test('calls useDataGridColumnsCellActions properly', () => {
const data = mockTimelineData.slice(0, 1);
const timestampFieldSpec = {
name: '@timestamp',
type: 'date',
aggregatable: true,
esTypes: ['date'],
searchable: true,
};
const wrapper = mount(
<TestProviders>
<DataTableComponent
cellActionsTriggerId="mockCellActionsTrigger"
{...props}
getFieldSpec={(name) =>
timestampFieldSpec.name === name ? timestampFieldSpec : undefined
}
data={data}
/>
</TestProviders>
@ -171,16 +182,7 @@ describe('DataTable', () => {
expect(mockUseDataGridColumnsCellActions).toHaveBeenCalledWith({
triggerId: 'mockCellActionsTrigger',
fields: [
{
name: '@timestamp',
type: 'date',
aggregatable: true,
esTypes: ['date'],
searchable: true,
subType: undefined,
},
],
fields: [timestampFieldSpec],
getCellValue: expect.any(Function),
metadata: {
scopeId: 'table-test',

View file

@ -42,6 +42,7 @@ import {
useDataGridColumnsCellActions,
UseDataGridColumnsCellActionsProps,
} from '@kbn/cell-actions';
import { FieldSpec } from '@kbn/data-views-plugin/common';
import { DataTableModel, DataTableState } from '../../store/data_table/types';
import { getColumnHeader, getColumnHeaders } from './column_headers/helpers';
@ -96,6 +97,7 @@ interface BaseDataTableProps {
rowHeightsOptions?: EuiDataGridRowHeightsOptions;
isEventRenderedView?: boolean;
getFieldBrowser: GetFieldBrowser;
getFieldSpec: (fieldName: string) => FieldSpec | undefined;
cellActionsTriggerId?: string;
}
@ -154,6 +156,7 @@ export const DataTableComponent = React.memo<DataTableProps>(
rowHeightsOptions,
isEventRenderedView = false,
getFieldBrowser,
getFieldSpec,
cellActionsTriggerId,
...otherProps
}) => {
@ -331,21 +334,20 @@ export const DataTableComponent = React.memo<DataTableProps>(
);
const cellActionsMetadata = useMemo(() => ({ scopeId: id }), [id]);
const cellActionsFields = useMemo<UseDataGridColumnsCellActionsProps['fields']>(
() =>
cellActionsTriggerId
? // TODO use FieldSpec object instead of column
columnHeaders.map((column) => ({
name: column.id,
type: column.type ?? '', // When type is an empty string all cell actions are incompatible
aggregatable: column.aggregatable ?? false,
searchable: column.searchable ?? false,
esTypes: column.esTypes ?? [],
subType: column.subType,
}))
? columnHeaders.map(
(column) =>
getFieldSpec(column.id) ?? {
name: column.id,
type: '', // When type is an empty string all cell actions are incompatible
aggregatable: false,
searchable: false,
}
)
: undefined,
[cellActionsTriggerId, columnHeaders]
[cellActionsTriggerId, columnHeaders, getFieldSpec]
);
const getCellValue = useCallback<UseDataGridColumnsCellActionsProps['getCellValue']>(

View file

@ -19,6 +19,7 @@
"@kbn/kibana-react-plugin",
"@kbn/kibana-utils-plugin",
"@kbn/i18n-react",
"@kbn/ui-actions-plugin"
"@kbn/ui-actions-plugin",
"@kbn/data-views-plugin"
]
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,115 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { i18n } from '@kbn/i18n';
import { Layer } from '../../../../../hooks/use_lens_attributes';
import { hostLensFormulas } from '../../../constants';
import { FormulaConfig } from '../../../types';
import { TOOLTIP } from './translations';
import { MetricLayerOptions } from '../../visualization_types/layers';
export interface KPIChartProps
extends Pick<TypedLensByValueInput, 'id' | 'title' | 'overrides' | 'style'> {
layers: Layer<MetricLayerOptions, FormulaConfig, 'data'>;
toolTip: string;
}
export const KPI_CHARTS: KPIChartProps[] = [
{
id: 'cpuUsage',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpuUsage.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.cpuUsage,
format: {
...hostLensFormulas.cpuUsage.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#F1D86F',
showTrendLine: true,
},
},
toolTip: TOOLTIP.cpuUsage,
},
{
id: 'normalizedLoad1m',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.normalizedLoad1m,
format: {
...hostLensFormulas.normalizedLoad1m.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#79AAD9',
showTrendLine: true,
},
},
toolTip: TOOLTIP.normalizedLoad1m,
},
{
id: 'memoryUsage',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memoryUsage.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.memoryUsage,
format: {
...hostLensFormulas.memoryUsage.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#A987D1',
showTrendLine: true,
},
},
toolTip: TOOLTIP.memoryUsage,
},
{
id: 'diskSpaceUsage',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title', {
defaultMessage: 'CPU Usage',
}),
layers: {
data: {
...hostLensFormulas.diskSpaceUsage,
format: {
...hostLensFormulas.diskSpaceUsage.format,
params: {
decimals: 1,
},
},
},
layerType: 'data',
options: {
backgroundColor: '#F5A35C',
showTrendLine: true,
},
},
toolTip: TOOLTIP.diskSpaceUsage,
},
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DataViewBase } from '@kbn/es-query';
export const getFilters = ({ id }: Pick<DataViewBase, 'id'>) => [
{
meta: {
index: id,
},
query: {
exists: {
field: 'host.name',
},
},
},
];

View file

@ -1,61 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { HostsLensMetricChartFormulas } from '../types';
import { TOOLTIP } from './translations';
export interface KPIChartProps {
title: string;
subtitle?: string;
trendLine?: boolean;
backgroundColor: string;
type: HostsLensMetricChartFormulas;
decimals?: number;
toolTip: string;
}
export const KPI_CHARTS: Array<Omit<KPIChartProps, 'loading' | 'subtitle' | 'style'>> = [
{
type: 'cpuUsage',
trendLine: true,
backgroundColor: '#F1D86F',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpuUsage.title', {
defaultMessage: 'CPU Usage',
}),
toolTip: TOOLTIP.cpuUsage,
},
{
type: 'normalizedLoad1m',
trendLine: true,
backgroundColor: '#79AAD9',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.normalizedLoad1m.title', {
defaultMessage: 'Normalized Load',
}),
toolTip: TOOLTIP.rx,
},
{
type: 'memoryUsage',
trendLine: true,
backgroundColor: '#A987D1',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memoryUsage.title', {
defaultMessage: 'Memory Usage',
}),
toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memoryUsage.tooltip', {
defaultMessage: 'Main memory usage excluding page cache.',
}),
},
{
type: 'diskSpaceUsage',
trendLine: true,
backgroundColor: '#F5A35C',
title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.diskSpaceUsage.title', {
defaultMessage: 'Disk Space Usage',
}),
toolTip: TOOLTIP.tx,
},
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FormulaPublicApi, PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { FormulaConfig, ChartColumn } from '../../../../types';
export class FormulaColumn implements ChartColumn {
constructor(private formulaConfig: FormulaConfig, private formulaAPI: FormulaPublicApi) {}
getFormulaConfig(): FormulaConfig {
return this.formulaConfig;
}
getData(
id: string,
baseLayer: PersistedIndexPatternLayer,
dataView: DataView
): PersistedIndexPatternLayer {
const { value, ...rest } = this.getFormulaConfig();
const formulaLayer = this.formulaAPI.insertOrReplaceFormulaColumn(
id,
{ formula: value, ...rest },
baseLayer,
dataView
);
if (!formulaLayer) {
throw new Error('Error generating the data layer for the chart');
}
return formulaLayer;
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { PersistedIndexPatternLayer } from '@kbn/lens-plugin/public';
import type { ReferenceBasedIndexPatternColumn } from '@kbn/lens-plugin/public/datasources/form_based/operations/definitions/column_types';
import type { FormulaConfig, ChartColumn } from '../../../../types';
export class ReferenceLineColumn implements ChartColumn {
constructor(private formulaConfig: FormulaConfig) {}
getFormulaConfig(): FormulaConfig {
return this.formulaConfig;
}
getData(id: string, baseLayer: PersistedIndexPatternLayer): PersistedIndexPatternLayer {
const { label, ...params } = this.getFormulaConfig();
return {
linkToLayers: [],
columnOrder: [...baseLayer.columnOrder, id],
columns: {
[id]: {
label: label ?? 'Reference',
dataType: 'number',
operationType: 'static_value',
isStaticValue: true,
isBucketed: false,
scale: 'ratio',
params,
references: [],
customLabel: true,
} as ReferenceBasedIndexPatternColumn,
},
sampling: 1,
incompleteColumns: {},
};
}
}

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { MetricLayer, type MetricLayerOptions } from './metric_layer';
export { XYDataLayer, type XYLayerOptions } from './xy_data_layer';
export { XYReferenceLinesLayer } from './xy_reference_lines_layer';
export { FormulaColumn as FormulaDataColumn } from './column/formula';
export { ReferenceLineColumn } from './column/reference_line';

View file

@ -0,0 +1,112 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/common';
import type {
FormulaPublicApi,
FormBasedPersistedState,
MetricVisualizationState,
PersistedIndexPatternLayer,
} from '@kbn/lens-plugin/public';
import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types';
import { getDefaultReferences, getHistogramColumn } from '../../utils';
import { FormulaColumn } from './column/formula';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export interface MetricLayerOptions {
backgroundColor?: string;
showTitle?: boolean;
showTrendLine?: boolean;
subtitle?: string;
}
interface MetricLayerConfig {
data: FormulaConfig;
options?: MetricLayerOptions;
formulaAPI: FormulaPublicApi;
}
export class MetricLayer implements ChartLayer<MetricVisualizationState> {
private column: ChartColumn;
constructor(private layerConfig: MetricLayerConfig) {
this.column = new FormulaColumn(layerConfig.data, layerConfig.formulaAPI);
}
getLayer(
layerId: string,
accessorId: string,
dataView: DataView
): FormBasedPersistedState['layers'] {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [HISTOGRAM_COLUMN_NAME],
columns: getHistogramColumn({
columnName: HISTOGRAM_COLUMN_NAME,
overrides: {
sourceField: dataView.timeFieldName,
params: {
interval: 'auto',
includeEmptyRows: true,
},
},
}),
sampling: 1,
};
return {
[layerId]: {
...this.column.getData(
accessorId,
{
columnOrder: [],
columns: {},
},
dataView
),
},
...(this.layerConfig.options?.showTrendLine
? {
[`${layerId}_trendline`]: {
linkToLayers: [layerId],
...this.column.getData(`${accessorId}_trendline`, baseLayer, dataView),
},
}
: {}),
};
}
getReference(layerId: string, dataView: DataView): SavedObjectReference[] {
return [
...getDefaultReferences(dataView, layerId),
...getDefaultReferences(dataView, `${layerId}_trendline`),
];
}
getLayerConfig(layerId: string, accessorId: string): MetricVisualizationState {
const { subtitle, backgroundColor, showTrendLine } = this.layerConfig.options ?? {};
return {
layerId,
layerType: 'data',
metricAccessor: accessorId,
color: backgroundColor,
subtitle,
showBar: false,
...(showTrendLine
? {
trendlineLayerId: `${layerId}_trendline`,
trendlineLayerType: 'metricTrendline',
trendlineMetricAccessor: `${accessorId}_trendline`,
trendlineTimeAccessor: HISTOGRAM_COLUMN_NAME,
}
: {}),
};
}
getName(): string | undefined {
return this.column.getFormulaConfig().label;
}
}

View file

@ -0,0 +1,106 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/common';
import type {
FormulaPublicApi,
FormBasedPersistedState,
PersistedIndexPatternLayer,
XYDataLayerConfig,
} from '@kbn/lens-plugin/public';
import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types';
import { getDefaultReferences, getHistogramColumn, getTopValuesColumn } from '../../utils';
import { FormulaColumn } from './column/formula';
const BREAKDOWN_COLUMN_NAME = 'aggs_breakdown';
const HISTOGRAM_COLUMN_NAME = 'x_date_histogram';
export interface XYLayerOptions {
breakdown?: {
size: number;
sourceField: string;
};
}
interface XYLayerConfig {
data: FormulaConfig[];
options?: XYLayerOptions;
formulaAPI: FormulaPublicApi;
}
export class XYDataLayer implements ChartLayer<XYDataLayerConfig> {
private column: ChartColumn[];
constructor(private layerConfig: XYLayerConfig) {
this.column = layerConfig.data.map((p) => new FormulaColumn(p, layerConfig.formulaAPI));
}
getName(): string | undefined {
return this.column[0].getFormulaConfig().label;
}
getBaseColumnColumn(dataView: DataView, options?: XYLayerOptions) {
return {
...getHistogramColumn({
columnName: HISTOGRAM_COLUMN_NAME,
overrides: {
sourceField: dataView.timeFieldName,
},
}),
...(options?.breakdown
? {
...getTopValuesColumn({
columnName: BREAKDOWN_COLUMN_NAME,
overrides: {
sourceField: options?.breakdown.sourceField,
breakdownSize: options?.breakdown.size,
},
}),
}
: {}),
};
}
getLayer(
layerId: string,
accessorId: string,
dataView: DataView
): FormBasedPersistedState['layers'] {
const baseLayer: PersistedIndexPatternLayer = {
columnOrder: [BREAKDOWN_COLUMN_NAME, HISTOGRAM_COLUMN_NAME],
columns: {
...this.getBaseColumnColumn(dataView, this.layerConfig.options),
},
};
return {
[layerId]: this.column.reduce(
(acc, curr, index) => ({
...acc,
...curr.getData(`${accessorId}_${index}`, acc, dataView),
}),
baseLayer
),
};
}
getReference(layerId: string, dataView: DataView): SavedObjectReference[] {
return getDefaultReferences(dataView, layerId);
}
getLayerConfig(layerId: string, accessorId: string): XYDataLayerConfig {
return {
layerId,
seriesType: 'line',
accessors: this.column.map((_, index) => `${accessorId}_${index}`),
yConfig: [],
layerType: 'data',
xAccessor: HISTOGRAM_COLUMN_NAME,
splitAccessor: BREAKDOWN_COLUMN_NAME,
};
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import type { DataView } from '@kbn/data-views-plugin/common';
import type {
FormBasedPersistedState,
PersistedIndexPatternLayer,
XYReferenceLineLayerConfig,
} from '@kbn/lens-plugin/public';
import type { ChartColumn, ChartLayer, FormulaConfig } from '../../../types';
import { getDefaultReferences } from '../../utils';
import { ReferenceLineColumn } from './column/reference_line';
interface XYReferenceLinesLayerConfig {
data: FormulaConfig[];
}
export class XYReferenceLinesLayer implements ChartLayer<XYReferenceLineLayerConfig> {
private column: ChartColumn[];
constructor(layerConfig: XYReferenceLinesLayerConfig) {
this.column = layerConfig.data.map((p) => new ReferenceLineColumn(p));
}
getName(): string | undefined {
return this.column[0].getFormulaConfig().label;
}
getLayer(
layerId: string,
accessorId: string,
dataView: DataView
): FormBasedPersistedState['layers'] {
const baseLayer = { columnOrder: [], columns: {} } as PersistedIndexPatternLayer;
return {
[`${layerId}_reference`]: this.column.reduce((acc, curr, index) => {
return {
...acc,
...curr.getData(`${accessorId}_${index}_reference_column`, acc, dataView),
};
}, baseLayer),
};
}
getReference(layerId: string, dataView: DataView): SavedObjectReference[] {
return getDefaultReferences(dataView, `${layerId}_reference`);
}
getLayerConfig(layerId: string, accessorId: string): XYReferenceLineLayerConfig {
return {
layerId: `${layerId}_reference`,
layerType: 'referenceLine',
accessors: this.column.map((_, index) => `${accessorId}_${index}_reference_column`),
yConfig: this.column.map((layer, index) => ({
color: layer.getFormulaConfig().color,
forAccessor: `${accessorId}_${index}_reference_column`,
axisMode: 'left',
})),
};
}
}

View file

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

View file

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

View file

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { FormBasedPersistedState, XYLayerConfig, XYState } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { SavedObjectReference } from '@kbn/core-saved-objects-common';
import { DEFAULT_LAYER_ID } from '../utils';
import type { Chart, ChartConfig, ChartLayer } from '../../types';
const ACCESSOR = 'formula_accessor';
export class XYChart implements Chart<XYState> {
constructor(private chartConfig: ChartConfig<Array<ChartLayer<XYLayerConfig>>>) {}
getVisualizationType(): string {
return 'lnsXY';
}
getLayers(): FormBasedPersistedState['layers'] {
return this.chartConfig.layers.reduce((acc, curr, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
const accessorId = `${ACCESSOR}_${index}`;
return {
...acc,
...curr.getLayer(layerId, accessorId, this.chartConfig.dataView),
};
}, {});
}
getVisualizationState(): XYState {
return getXYVisualizationState({
layers: [
...this.chartConfig.layers.map((layerItem, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
const accessorId = `${ACCESSOR}_${index}`;
return layerItem.getLayerConfig(layerId, accessorId);
}),
],
});
}
getReferences(): SavedObjectReference[] {
return this.chartConfig.layers.flatMap((p, index) => {
const layerId = `${DEFAULT_LAYER_ID}_${index}`;
return p.getReference(layerId, this.chartConfig.dataView);
});
}
getDataView(): DataView {
return this.chartConfig.dataView;
}
getTitle(): string {
return this.chartConfig.title ?? this.chartConfig.layers[0].getName() ?? '';
}
}
export const getXYVisualizationState = (
custom: Omit<Partial<XYState>, 'layers'> & { layers: XYState['layers'] }
): XYState => ({
legend: {
isVisible: false,
position: 'right',
showSingleSeries: false,
},
valueLabels: 'show',
fittingFunction: 'Zero',
curveType: 'LINEAR',
yLeftScale: 'linear',
axisTitlesVisibilitySettings: {
x: false,
yLeft: false,
yRight: true,
},
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
labelsOrientation: {
x: 0,
yLeft: 0,
yRight: 0,
},
gridlinesVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
preferredSeriesType: 'line',
valuesInLegend: false,
emphasizeFitting: true,
hideEndzones: true,
...custom,
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -18,8 +18,6 @@ export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations';
export const BASE_API_URL = '/api/lens';
export const LENS_EDIT_BY_VALUE = 'edit_by_value';
export const ENABLE_SQL = 'discover:enableSql';
export const PieChartTypes = {
PIE: 'pie',
DONUT: 'donut',

View file

@ -17,7 +17,7 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import moment from 'moment';
import { LENS_APP_LOCATOR } from '../../common/locator/locator';
import { ENABLE_SQL, LENS_APP_NAME } from '../../common/constants';
import { LENS_APP_NAME } from '../../common/constants';
import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types';
import { toggleSettingsMenuOpen } from './settings_menu';
import {
@ -986,13 +986,6 @@ export const LensTopNavMenu = ({
]
);
// setting that enables/disables SQL
const isSQLModeEnabled = uiSettings.get(ENABLE_SQL);
const supportedTextBasedLanguages = [];
if (isSQLModeEnabled) {
supportedTextBasedLanguages.push('SQL');
}
const dataViewPickerProps: DataViewPickerProps = {
trigger: {
label: currentIndexPattern?.getName?.() || '',
@ -1052,7 +1045,6 @@ export const LensTopNavMenu = ({
indexPatternService.replaceDataViewId(updatedDataViewStub);
}
},
textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
};
const textBasedLanguageModeErrors = getUserMessages('textBasedLanguagesQueryInput', {

View file

@ -79,6 +79,7 @@ import { useAlertBulkActions } from './use_alert_bulk_actions';
import type { BulkActionsProp } from '../toolbar/bulk_actions/types';
import { StatefulEventContext } from './stateful_event_context';
import { defaultUnit } from '../toolbar/unit';
import { useGetFieldSpec } from '../../hooks/use_get_field_spec';
const storage = new Storage(localStorage);
@ -184,6 +185,8 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
loading: isLoadingIndexPattern,
} = useSourcererDataView(sourcererScope);
const getFieldSpec = useGetFieldSpec(sourcererScope);
const { globalFullScreen } = useGlobalFullScreen();
const editorActionsRef = useRef<FieldEditorActions>(null);
@ -602,6 +605,7 @@ const StatefulEventsViewerComponent: React.FC<EventsViewerProps & PropsFromRedux
isEventRenderedView={tableView === 'eventRenderedView'}
rowHeightsOptions={rowHeightsOptions}
getFieldBrowser={getFieldBrowser}
getFieldSpec={getFieldSpec}
/>
</StatefulEventContext.Provider>
</ScrollableFlexItem>

View file

@ -7,6 +7,7 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import React from 'react';
import { useUserData } from '../../../../../detections/components/user_info';
import { useAddPrebuiltRulesTableContext } from './add_prebuilt_rules_table_context';
import * as i18n from './translations';
@ -15,6 +16,8 @@ export const AddPrebuiltRulesHeaderButtons = () => {
state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages },
actions: { installAllRules, installSelectedRules },
} = useAddPrebuiltRulesTableContext();
const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData();
const canUserEditRules = canUserCRUD && !isUserDataLoading;
const isRulesAvailableForInstall = rules.length > 0;
const numberOfSelectedRules = selectedRules.length ?? 0;
@ -29,7 +32,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
<EuiFlexItem grow={false}>
<EuiButton
onClick={installSelectedRules}
disabled={isRequestInProgress}
disabled={!canUserEditRules || isRequestInProgress}
data-test-subj="installSelectedRulesButton"
>
{i18n.INSTALL_SELECTED_RULES(numberOfSelectedRules)}
@ -43,7 +46,7 @@ export const AddPrebuiltRulesHeaderButtons = () => {
iconType="plusInCircle"
data-test-subj="installAllRulesButton"
onClick={installAllRules}
disabled={!isRulesAvailableForInstall || isRequestInProgress}
disabled={!canUserEditRules || !isRulesAvailableForInstall || isRequestInProgress}
>
{i18n.INSTALL_ALL}
{isRuleInstalling ? <EuiLoadingSpinner size="s" /> : undefined}

View file

@ -6,6 +6,7 @@
*/
import React, { useMemo } from 'react';
import { useUserData } from '../../../../detections/components/user_info';
import { TabNavigation } from '../../../../common/components/navigation/tab_navigation';
import { usePrebuiltRulesStatus } from '../../../rule_management/logic/prebuilt_rules/use_prebuilt_rules_status';
import { useRuleManagementFilters } from '../../../rule_management/logic/use_rule_management_filters';
@ -21,26 +22,14 @@ export const RulesTableToolbar = React.memo(() => {
const { data: ruleManagementFilters } = useRuleManagementFilters();
const { data: prebuiltRulesStatus } = usePrebuiltRulesStatus();
const [{ loading, canUserCRUD }] = useUserData();
const installedTotal =
(ruleManagementFilters?.rules_summary.custom_count ?? 0) +
(ruleManagementFilters?.rules_summary.prebuilt_installed_count ?? 0);
const updateTotal = prebuiltRulesStatus?.num_prebuilt_rules_to_upgrade ?? 0;
const ruleUpdateTab = useMemo(
() => ({
[AllRulesTabs.updates]: {
id: AllRulesTabs.updates,
name: i18n.RULE_UPDATES_TAB,
disabled: false,
href: `/rules/${AllRulesTabs.updates}`,
isBeta: updateTotal > 0,
betaOptions: {
text: `${updateTotal}`,
},
},
}),
[updateTotal]
);
const shouldDisplayRuleUpdatesTab = !loading && canUserCRUD && updateTotal > 0;
const ruleTabs = useMemo(
() => ({
@ -64,9 +53,22 @@ export const RulesTableToolbar = React.memo(() => {
text: `${installedTotal}`,
},
},
...(updateTotal > 0 ? ruleUpdateTab : {}),
...(shouldDisplayRuleUpdatesTab
? {
[AllRulesTabs.updates]: {
id: AllRulesTabs.updates,
name: i18n.RULE_UPDATES_TAB,
disabled: false,
href: `/rules/${AllRulesTabs.updates}`,
isBeta: updateTotal > 0,
betaOptions: {
text: `${updateTotal}`,
},
},
}
: {}),
}),
[installedTotal, ruleUpdateTab, updateTotal]
[installedTotal, updateTotal, shouldDisplayRuleUpdatesTab]
);
return <TabNavigation navTabs={ruleTabs} />;

View file

@ -7,6 +7,7 @@
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import React from 'react';
import { useUserData } from '../../../../../detections/components/user_info';
import * as i18n from './translations';
import { useUpgradePrebuiltRulesTableContext } from './upgrade_prebuilt_rules_table_context';
@ -15,6 +16,8 @@ export const UpgradePrebuiltRulesTableButtons = () => {
state: { rules, selectedRules, loadingRules, isRefetching, isUpgradingSecurityPackages },
actions: { upgradeAllRules, upgradeSelectedRules },
} = useUpgradePrebuiltRulesTableContext();
const [{ loading: isUserDataLoading, canUserCRUD }] = useUserData();
const canUserEditRules = canUserCRUD && !isUserDataLoading;
const isRulesAvailableForUpgrade = rules.length > 0;
const numberOfSelectedRules = selectedRules.length ?? 0;
@ -29,7 +32,7 @@ export const UpgradePrebuiltRulesTableButtons = () => {
<EuiFlexItem grow={false}>
<EuiButton
onClick={upgradeSelectedRules}
disabled={isRequestInProgress}
disabled={!canUserEditRules || isRequestInProgress}
data-test-subj="upgradeSelectedRulesButton"
>
<>
@ -44,7 +47,7 @@ export const UpgradePrebuiltRulesTableButtons = () => {
fill
iconType="plusInCircle"
onClick={upgradeAllRules}
disabled={!isRulesAvailableForUpgrade || isRequestInProgress}
disabled={!canUserEditRules || !isRulesAvailableForUpgrade || isRequestInProgress}
data-test-subj="upgradeAllRulesButton"
>
{i18n.UPDATE_ALL}

View file

@ -107,7 +107,7 @@ const RulesPageComponent: React.FC = () => {
<SuperHeader>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}>
<EuiFlexItem grow={false}>
<AddElasticRulesButton />
<AddElasticRulesButton isDisabled={!canUserCRUD || loading} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip position="top" content={i18n.UPLOAD_VALUE_LISTS_TOOLTIP}>

View file

@ -17,12 +17,14 @@ import { usePrebuiltRulesStatus } from '../../../../detection_engine/rule_manage
interface AddElasticRulesButtonProps {
'data-test-subj'?: string;
fill?: boolean;
isDisabled: boolean;
showBadge?: boolean;
}
export const AddElasticRulesButton = ({
'data-test-subj': dataTestSubj = 'addElasticRulesButton',
fill,
isDisabled,
showBadge = true,
}: AddElasticRulesButtonProps) => {
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps();
@ -43,6 +45,7 @@ export const AddElasticRulesButton = ({
color={'primary'}
onClick={onClickLink}
data-test-subj={dataTestSubj}
isDisabled={isDisabled}
>
{i18n.ADD_ELASTIC_RULES}
{newRulesCount > 0 && showBadge && (

View file

@ -8,6 +8,7 @@
import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { memo } from 'react';
import styled from 'styled-components';
import { useUserData } from '../../user_info';
import { AddElasticRulesButton } from './add_elastic_rules_button';
import * as i18n from './translations';
@ -18,6 +19,7 @@ const EmptyPrompt = styled(EuiEmptyPrompt)`
EmptyPrompt.displayName = 'EmptyPrompt';
const PrePackagedRulesPromptComponent = () => {
const [{ loading, canUserCRUD }] = useUserData();
return (
<EmptyPrompt
data-test-subj="rulesEmptyPrompt"
@ -27,6 +29,7 @@ const PrePackagedRulesPromptComponent = () => {
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<AddElasticRulesButton
isDisabled={!canUserCRUD || loading}
fill={true}
data-test-subj="add-elastc-rules-empty-empty-prompt-button"
showBadge={false}

View file

@ -5,18 +5,17 @@
* 2.0.
*/
import type { BrowserField, TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import type { TimelineNonEcsData } from '@kbn/timelines-plugin/common';
import type { AlertsTableConfigurationRegistry } from '@kbn/triggers-actions-ui-plugin/public/types';
import { useCallback, useMemo } from 'react';
import { TableId, tableDefaults, dataTableSelectors } from '@kbn/securitysolution-data-table';
import { getAllFieldsByName } from '../../../common/containers/source';
import type { UseDataGridColumnsSecurityCellActionsProps } from '../../../common/components/cell_actions';
import { useDataGridColumnsSecurityCellActions } from '../../../common/components/cell_actions';
import { SecurityCellActionsTrigger, SecurityCellActionType } from '../../../actions/constants';
import { VIEW_SELECTION } from '../../../../common/constants';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useGetFieldSpec } from '../../../common/hooks/use_get_field_spec';
export const getUseCellActionsHook = (tableId: TableId) => {
const useCellActions: AlertsTableConfigurationRegistry['useCellActions'] = ({
@ -24,7 +23,7 @@ export const getUseCellActionsHook = (tableId: TableId) => {
data,
dataGridRef,
}) => {
const { browserFields } = useSourcererDataView(SourcererScopeName.detections);
const getFieldSpec = useGetFieldSpec(SourcererScopeName.detections);
/**
* There is difference between how `triggers actions` fetched data v/s
* how security solution fetches data via timelineSearchStrategy
@ -35,7 +34,6 @@ export const getUseCellActionsHook = (tableId: TableId) => {
*
*/
const browserFieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]);
const finalData = useMemo(
() =>
(data as TimelineNonEcsData[][]).map((row) =>
@ -66,19 +64,16 @@ export const getUseCellActionsHook = (tableId: TableId) => {
if (viewMode === VIEW_SELECTION.eventRenderedView) {
return undefined;
}
return columns.map((column) => {
// TODO use FieldSpec object instead of browserField
const browserField: Partial<BrowserField> | undefined = browserFieldsByName[column.id];
return {
name: column.id,
type: browserField?.type ?? '', // When type is an empty string all cell actions are incompatible
esTypes: browserField?.esTypes ?? [],
aggregatable: browserField?.aggregatable ?? false,
searchable: browserField?.searchable ?? false,
subType: browserField?.subType,
};
});
}, [browserFieldsByName, columns, viewMode]);
return columns.map(
(column) =>
getFieldSpec(column.id) ?? {
name: '',
type: '', // When type is an empty string all cell actions are incompatible
aggregatable: false,
searchable: false,
}
);
}, [getFieldSpec, columns, viewMode]);
const getCellValue = useCallback<UseDataGridColumnsSecurityCellActionsProps['getCellValue']>(
(fieldName, rowIndex) => {

View file

@ -35,7 +35,7 @@ export const performRuleInstallationRoute = (router: SecuritySolutionPluginRoute
body: buildRouteValidation(PerformRuleInstallationRequestBody),
},
options: {
tags: ['access:securitySolution'],
tags: ['access:securitySolution-all'],
},
},
async (context, request, response) => {

View file

@ -39,7 +39,7 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) =>
body: buildRouteValidation(PerformRuleUpgradeRequestBody),
},
options: {
tags: ['access:securitySolution'],
tags: ['access:securitySolution-all'],
},
},
async (context, request, response) => {

View file

@ -6,6 +6,7 @@
*/
import expect from '@kbn/expect';
import { DebugState } from '@elastic/charts';
import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper';
import { FtrProviderContext } from '../../ftr_provider_context';
@ -28,6 +29,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
'header',
'unifiedFieldList',
]);
const elasticChart = getService('elasticChart');
const monacoEditor = getService('monacoEditor');
const defaultSettings = {
@ -38,6 +40,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await PageObjects.timePicker.setDefaultAbsoluteRange();
}
function assertMatchesExpectedData(state: DebugState) {
expect(state.legend?.items.map(({ name }) => name).sort()).to.eql([
'css',
'gif',
'jpg',
'php',
'png',
]);
}
describe('discover field visualize button', () => {
before(async () => {
await kibanaServer.uiSettings.replace(defaultSettings);
@ -147,7 +159,33 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(await testSubjects.exists('partitionVisChart')).to.be(true);
});
it('should visualize correctly text based language queries in Lens', async () => {
it('should allow changing dimensions', async () => {
await elasticChart.setNewChartUiDebugFlag(true);
await PageObjects.discover.selectTextBaseLang('SQL');
await PageObjects.header.waitUntilLoadingHasFinished();
await monacoEditor.setCodeEditorValue(
'SELECT extension, AVG("bytes") as average FROM "logstash-*" GROUP BY extension'
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.chooseLensChart('Bar vertical stacked');
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('unifiedHistogramEditFlyoutVisualization');
expect(await testSubjects.exists('xyVisChart')).to.be(true);
expect(await PageObjects.lens.canRemoveDimension('lnsXY_xDimensionPanel')).to.equal(true);
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
field: 'extension',
});
await PageObjects.header.waitUntilLoadingHasFinished();
const data = await PageObjects.lens.getCurrentChartDebugStateForVizType('xyVisChart');
assertMatchesExpectedData(data!);
});
it('should visualize correctly text based language queries in Lenss', async () => {
await PageObjects.discover.selectTextBaseLang('SQL');
await PageObjects.header.waitUntilLoadingHasFinished();
await monacoEditor.setCodeEditorValue(
@ -185,7 +223,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
it('should save correctly chart to dashboard', async () => {
it('should save and edit chart in the dashboard on the fly', async () => {
await PageObjects.discover.selectTextBaseLang('SQL');
await PageObjects.header.waitUntilLoadingHasFinished();
await monacoEditor.setCodeEditorValue(
@ -193,12 +231,28 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.chooseLensChart('Bar vertical stacked');
await testSubjects.click('TextBasedLangEditor-expand');
await testSubjects.click('unifiedHistogramSaveVisualization');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.saveModal('TextBasedChart', false, false, false, 'new');
await testSubjects.existOrFail('embeddablePanelHeading-TextBasedChart');
await elasticChart.setNewChartUiDebugFlag(true);
await PageObjects.header.waitUntilLoadingHasFinished();
await testSubjects.click('embeddablePanelToggleMenuIcon');
await testSubjects.click('embeddablePanelAction-ACTION_CONFIGURE_IN_LENS');
await PageObjects.header.waitUntilLoadingHasFinished();
expect(await PageObjects.lens.canRemoveDimension('lnsXY_xDimensionPanel')).to.equal(true);
await PageObjects.lens.removeDimension('lnsXY_xDimensionPanel');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
field: 'extension',
});
await PageObjects.header.waitUntilLoadingHasFinished();
const data = await PageObjects.lens.getCurrentChartDebugStateForVizType('xyVisChart');
assertMatchesExpectedData(data!);
});
});
}

View file

@ -59,7 +59,6 @@ export default ({ getService, loadTestFile, getPageObjects }: FtrProviderContext
loadTestFile(require.resolve('./persistent_context')); // 1m
loadTestFile(require.resolve('./table_dashboard')); // 3m 10s
loadTestFile(require.resolve('./table')); // 1m 40s
loadTestFile(require.resolve('./text_based_languages')); // 3m 40s
loadTestFile(require.resolve('./fields_list')); // 2m 7s
loadTestFile(require.resolve('./layer_actions')); // 1m 45s
loadTestFile(require.resolve('./field_formatters')); // 1m 30s

View file

@ -1,235 +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 { DebugState } from '@elastic/charts';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects([
'visualize',
'lens',
'header',
'unifiedSearch',
'dashboard',
'common',
]);
const browser = getService('browser');
const elasticChart = getService('elasticChart');
const queryBar = getService('queryBar');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const monacoEditor = getService('monacoEditor');
function assertMatchesExpectedData(state: DebugState) {
expect(state.axes?.x![0].labels.sort()).to.eql(['css', 'gif', 'jpg', 'php', 'png']);
}
const defaultSettings = {
'discover:enableSql': true,
defaultIndex: 'log*',
};
async function switchToTextBasedLanguage(language: string) {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickVisType('lens');
await PageObjects.lens.goToTimeRange();
await elasticChart.setNewChartUiDebugFlag(true);
await PageObjects.lens.switchToTextBasedLanguage(language);
await PageObjects.header.waitUntilLoadingHasFinished();
}
describe('lens text based language tests', () => {
before(async () => {
await kibanaServer.uiSettings.update(defaultSettings);
});
it('should navigate to text based languages mode correctly', async () => {
await switchToTextBasedLanguage('SQL');
expect(await testSubjects.exists('showQueryBarMenu')).to.be(false);
expect(await testSubjects.exists('addFilter')).to.be(false);
await testSubjects.click('TextBasedLangEditor-expand');
const textBasedQuery = await monacoEditor.getCodeEditorValue();
expect(textBasedQuery).to.be('SELECT * FROM "log*"');
await testSubjects.click('TextBasedLangEditor-minimize');
});
it('should allow adding and using a field', async () => {
await monacoEditor.setCodeEditorValue(
'SELECT extension, AVG("bytes") as average FROM "logstash-*" GROUP BY extension'
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.switchToVisualization('lnsMetric');
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsMetric_primaryMetricDimensionPanel > lns-empty-dimension',
field: 'average',
});
await PageObjects.lens.waitForVisualization('mtrVis');
const metricData = await PageObjects.lens.getMetricVisualizationData();
expect(metricData[0].title).to.eql('average');
});
it('should allow switching to another chart', async () => {
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.switchToVisualization('bar');
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
field: 'extension',
});
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
field: 'average',
});
await PageObjects.lens.waitForVisualization('xyVisChart');
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
assertMatchesExpectedData(data!);
});
it('should be possible to share a URL of a visualization with text-based language', async () => {
const url = await PageObjects.lens.getUrl('snapshot');
await browser.openNewTab();
const [lensWindowHandler] = await browser.getAllWindowHandles();
await browser.navigateTo(url);
// check that it's the same configuration in the new URL when ready
await PageObjects.header.waitUntilLoadingHasFinished();
expect(
await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0, true)
).to.eql('extension');
expect(
await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel', 0, true)
).to.eql('average');
await browser.closeCurrentWindow();
await browser.switchToWindow(lensWindowHandler);
});
it('should be possible to download a visualization with text-based language', async () => {
await PageObjects.lens.setCSVDownloadDebugFlag(true);
await PageObjects.lens.openCSVDownloadShare();
const csv = await PageObjects.lens.getCSVContent();
expect(csv).to.be.ok();
expect(Object.keys(csv!)).to.have.length(1);
await PageObjects.lens.setCSVDownloadDebugFlag(false);
});
it('should allow adding an text based languages chart to a dashboard', async () => {
await PageObjects.lens.switchToVisualization('lnsMetric');
await PageObjects.lens.waitForVisualization('mtrVis');
await PageObjects.lens.removeDimension('lnsMetric_breakdownByDimensionPanel');
await PageObjects.lens.waitForVisualization('mtrVis');
const metricData = await PageObjects.lens.getMetricVisualizationData();
expect(metricData[0].value).to.eql('5,699.406');
expect(metricData[0].title).to.eql('average');
await PageObjects.lens.save('New text based languages viz', false, false, false, 'new');
await PageObjects.dashboard.waitForRenderComplete();
expect(metricData[0].value).to.eql('5,699.406');
const panelCount = await PageObjects.dashboard.getPanelCount();
expect(panelCount).to.eql(1);
});
it('should allow saving the text based languages chart into a saved object', async () => {
await switchToTextBasedLanguage('SQL');
await monacoEditor.setCodeEditorValue(
'SELECT extension, AVG("bytes") as average FROM "logstash-*" GROUP BY extension'
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
field: 'extension',
});
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
field: 'average',
});
await PageObjects.lens.waitForVisualization('xyVisChart');
await PageObjects.lens.save('Lens with text based language');
await PageObjects.lens.waitForVisualization('xyVisChart');
const data = await PageObjects.lens.getCurrentChartDebugState('xyVisChart');
assertMatchesExpectedData(data!);
});
it('should allow to return to the dataview mode', async () => {
await PageObjects.lens.switchDataPanelIndexPattern('logstash-*', true);
expect(await testSubjects.exists('addFilter')).to.be(true);
expect(await queryBar.getQueryString()).to.be('');
});
it('should allow using an index pattern that is not translated to a dataview', async () => {
await switchToTextBasedLanguage('SQL');
await testSubjects.click('TextBasedLangEditor-expand');
await monacoEditor.setCodeEditorValue(
'SELECT extension, AVG("bytes") as average FROM "logstash*" GROUP BY extension'
);
await testSubjects.click('querySubmitButton');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.switchToVisualization('lnsMetric');
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsMetric_primaryMetricDimensionPanel > lns-empty-dimension',
field: 'average',
});
await PageObjects.lens.waitForVisualization('mtrVis');
const metricData = await PageObjects.lens.getMetricVisualizationData();
expect(metricData[0].title).to.eql('average');
});
it('should be possible to share a URL of a visualization with text-based language that points to an index pattern', async () => {
// TODO: there's some state leakage in Lens when passing from a XY chart to new Metric chart
// which generates a wrong state (even tho it looks to work, starting fresh with such state breaks the editor)
await PageObjects.lens.removeLayer();
await PageObjects.lens.switchToVisualization('bar');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
field: 'extension',
});
await PageObjects.lens.configureTextBasedLanguagesDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
field: 'average',
});
const url = await PageObjects.lens.getUrl('snapshot');
await browser.openNewTab();
const [lensWindowHandler] = await browser.getAllWindowHandles();
await browser.navigateTo(url);
// check that it's the same configuration in the new URL when ready
await PageObjects.header.waitUntilLoadingHasFinished();
expect(
await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel', 0, true)
).to.eql('extension');
expect(
await PageObjects.lens.getDimensionTriggerText('lnsXY_yDimensionPanel', 0, true)
).to.eql('average');
await browser.closeCurrentWindow();
await browser.switchToWindow(lensWindowHandler);
});
it('should be possible to download a visualization with text-based language that points to an index pattern', async () => {
await PageObjects.lens.setCSVDownloadDebugFlag(true);
await PageObjects.lens.openCSVDownloadShare();
const csv = await PageObjects.lens.getCSVContent();
expect(csv).to.be.ok();
expect(Object.keys(csv!)).to.have.length(1);
await PageObjects.lens.setCSVDownloadDebugFlag(false);
});
});
}

View file

@ -1078,6 +1078,11 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
return el.getVisibleText();
},
async getCurrentChartDebugStateForVizType(visType: string) {
await this.waitForVisualization(visType);
return await elasticChart.getChartDebugData(visType);
},
/**
* Gets text of the specified datatable cell
*
@ -1454,7 +1459,7 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
}
if (!opts.keepOpen) {
await this.closeDimensionEditor();
await testSubjects.click('collapseFlyoutButton');
}
},

View file

@ -7,12 +7,18 @@
import { CaseSeverity } from '@kbn/cases-plugin/common/api';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { createAndUploadFile } from '../../../../cases_api_integration/common/lib/api';
import { SECURITY_SOLUTION_FILE_KIND } from '../../../../cases_api_integration/common/lib/constants';
export default function ({ getPageObject, getService, getPageObjects }: FtrProviderContext) {
const cases = getService('cases');
const commonScreenshots = getService('commonScreenshots');
const screenshotDirectories = ['response_ops_docs', 'security_cases'];
const pageObjects = getPageObjects(['common', 'header']);
const screenshotDirectories = ['response_ops_docs', 'security_cases'];
const supertest = getService('supertest');
const testSubjects = getService('testSubjects');
let caseIdSuspiciousEmail: string;
let caseOwnerSuspiciousEmail: string;
describe('list view', function () {
before(async () => {
@ -24,12 +30,14 @@ export default function ({ getPageObject, getService, getPageObjects }: FtrProvi
severity: CaseSeverity.HIGH,
});
await cases.api.createCase({
const caseSuspiciousEmail = await cases.api.createCase({
title: 'Suspicious emails reported',
tags: ['email', 'phishing'],
description: 'Test.',
description: 'Several employees have received suspicious emails from an unknown address.',
owner: 'securitySolution',
});
caseIdSuspiciousEmail = caseSuspiciousEmail.id;
caseOwnerSuspiciousEmail = caseSuspiciousEmail.owner;
await cases.api.createCase({
title: 'Malware investigation',
@ -38,6 +46,20 @@ export default function ({ getPageObject, getService, getPageObjects }: FtrProvi
owner: 'securitySolution',
severity: CaseSeverity.MEDIUM,
});
await createAndUploadFile({
supertest,
createFileParams: {
name: 'testfile',
kind: SECURITY_SOLUTION_FILE_KIND,
mimeType: 'image/png',
meta: {
caseIds: [caseIdSuspiciousEmail],
owner: [caseOwnerSuspiciousEmail],
},
},
data: 'abc',
});
});
after(async () => {
@ -49,5 +71,16 @@ export default function ({ getPageObject, getService, getPageObjects }: FtrProvi
await pageObjects.header.waitUntilLoadingHasFinished();
await commonScreenshots.takeScreenshot('cases-home-page', screenshotDirectories, 1700, 1024);
});
it('case details screenshot', async () => {
await pageObjects.common.navigateToApp('security', {
path: `cases/${caseIdSuspiciousEmail}`,
});
await commonScreenshots.takeScreenshot('cases-ui-open', screenshotDirectories, 1400, 1024);
const filesTab = await testSubjects.find('case-view-tab-title-files');
await filesTab.click();
await commonScreenshots.takeScreenshot('cases-files', screenshotDirectories, 1400, 1024);
});
});
}