mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Merge branch 'main' into kibana_context
This commit is contained in:
commit
07103a02dd
76 changed files with 1971 additions and 1249 deletions
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -84,6 +84,7 @@ export const DataTable = () => {
|
|||
<StoryProviders>
|
||||
<DataTableComponent
|
||||
browserFields={{}}
|
||||
getFieldSpec={() => undefined}
|
||||
data={mockTimelineData}
|
||||
id={TableId.test}
|
||||
renderCellValue={StoryCellRenderer}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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']>(
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
166
x-pack/plugins/infra/public/common/visualizations/lens/README.md
Normal file
166
x-pack/plugins/infra/public/common/visualizations/lens/README.md
Normal 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}
|
||||
...
|
||||
/>
|
||||
```
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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())),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}: {
|
||||
|
|
|
@ -5,5 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { LineChart } from './line_chart';
|
||||
export { XYChart } from './xy_chart';
|
||||
export { MetricChart } from './metric_chart';
|
||||
|
||||
export * from './layers';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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: {},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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',
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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 ?? '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
|
@ -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'>;
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -35,7 +35,7 @@ export const performRuleInstallationRoute = (router: SecuritySolutionPluginRoute
|
|||
body: buildRouteValidation(PerformRuleInstallationRequestBody),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
tags: ['access:securitySolution-all'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
|
|
|
@ -39,7 +39,7 @@ export const performRuleUpgradeRoute = (router: SecuritySolutionPluginRouter) =>
|
|||
body: buildRouteValidation(PerformRuleUpgradeRequestBody),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:securitySolution'],
|
||||
tags: ['access:securitySolution-all'],
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
|
|
|
@ -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!);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue