Remove Exploratory View components from Observability (#155629)

Co-authored-by: shahzad31 <shahzad31comp@gmail.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Coen Warmer 2023-04-26 09:21:20 +02:00 committed by GitHub
parent 7534cc7851
commit a07bdc5da9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
180 changed files with 817 additions and 18916 deletions

View file

@ -95,7 +95,7 @@ pageLoadAssetSize:
newsfeed: 42228
observability: 95000
observabilityOnboarding: 19573
observabilityShared: 21266
observabilityShared: 36643
osquery: 107090
painlessLab: 179748
presentationUtil: 58834

View file

@ -20,7 +20,7 @@ import {
} from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { AllSeries } from '@kbn/observability-plugin/public';
import { AllSeries } from '@kbn/exploratory-view-plugin/public';
import { StartDependencies } from './plugin';
export const App = (props: {

View file

@ -55,16 +55,10 @@ export function SeriesColorPicker({ seriesId, series }: { seriesId: number; seri
);
}
const PICK_A_COLOR_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.pickColor',
{
defaultMessage: 'Pick a color',
}
);
const PICK_A_COLOR_LABEL = i18n.translate('xpack.exploratoryView.pickColor', {
defaultMessage: 'Pick a color',
});
const EDIT_SERIES_COLOR_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.editSeriesColor',
{
defaultMessage: 'Edit color for series',
}
);
const EDIT_SERIES_COLOR_LABEL = i18n.translate('xpack.exploratoryView.editSeriesColor', {
defaultMessage: 'Edit color for series',
});

View file

@ -21,7 +21,7 @@ export function getAlertsKPIConfig({ spaceId }: ConfigProps): SeriesConfig {
defaultSeriesType: 'line',
seriesTypes: [],
xAxisColumn: {
label: i18n.translate('xpack.exploratoryView.exploratoryView.alerts.alertStarted', {
label: i18n.translate('xpack.exploratoryView.alerts.alertStarted', {
defaultMessage: 'Timestamp',
}),
dataType: 'date',

View file

@ -17,7 +17,7 @@ export function getLogsKPIConfig(configProps: ConfigProps): SeriesConfig {
defaultSeriesType: 'bar',
seriesTypes: [],
xAxisColumn: {
label: i18n.translate('xpack.exploratoryView.exploratoryView.logs.logRateXAxisLabel', {
label: i18n.translate('xpack.exploratoryView.logs.logRateXAxisLabel', {
defaultMessage: 'Timestamp',
}),
dataType: 'date',
@ -28,7 +28,7 @@ export function getLogsKPIConfig(configProps: ConfigProps): SeriesConfig {
},
yAxisColumns: [
{
label: i18n.translate('xpack.exploratoryView.exploratoryView.logs.logRateYAxisLabel', {
label: i18n.translate('xpack.exploratoryView.logs.logRateYAxisLabel', {
defaultMessage: 'Log rate per minute',
}),
dataType: 'number',

View file

@ -175,6 +175,6 @@ function EmptyState({ height }: { height?: string }) {
);
}
const NO_DATA_LABEL = i18n.translate('xpack.exploratoryView.overview.exploratoryView.noData', {
const NO_DATA_LABEL = i18n.translate('xpack.exploratoryView.noData', {
defaultMessage: 'No data',
});

View file

@ -204,27 +204,18 @@ const ShowPreview = styled(EuiButtonEmpty)`
bottom: 34px;
`;
const PREVIEW_LABEL = i18n.translate('xpack.exploratoryView.overview.exploratoryView.preview', {
const PREVIEW_LABEL = i18n.translate('xpack.exploratoryView.preview', {
defaultMessage: 'Preview',
});
const HIDE_CHART_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.hideChart',
{
defaultMessage: 'Hide chart',
}
);
const HIDE_CHART_LABEL = i18n.translate('xpack.exploratoryView.hideChart', {
defaultMessage: 'Hide chart',
});
const SHOW_CHART_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.showChart',
{
defaultMessage: 'Show chart',
}
);
const SHOW_CHART_LABEL = i18n.translate('xpack.exploratoryView.showChart', {
defaultMessage: 'Show chart',
});
const LENS_NOT_AVAILABLE = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.lensDisabled',
{
defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.',
}
);
const LENS_NOT_AVAILABLE = i18n.translate('xpack.exploratoryView.lensDisabled', {
defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.',
});

View file

@ -32,9 +32,6 @@ export function RefreshButton() {
);
}
export const REFRESH_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.refresh',
{
defaultMessage: 'Refresh',
}
);
export const REFRESH_LABEL = i18n.translate('xpack.exploratoryView.refresh', {
defaultMessage: 'Refresh',
});

View file

@ -56,7 +56,7 @@ export function ExploratoryViewPage({
useBreadcrumbs(
[
{
text: i18n.translate('xpack.exploratoryView.overview.exploratoryView', {
text: i18n.translate('xpack.exploratoryView.overview', {
defaultMessage: 'Explore data',
}),
},

View file

@ -17,39 +17,30 @@ export enum DataTypes {
}
export const DataTypesLabels: Record<string, string> = {
[DataTypes.UX]: i18n.translate('xpack.exploratoryView.overview.exploratoryView.uxLabel', {
[DataTypes.UX]: i18n.translate('xpack.exploratoryView.uxLabel', {
defaultMessage: 'User experience (RUM)',
}),
[DataTypes.SYNTHETICS]: i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.syntheticsLabel',
{
defaultMessage: 'Synthetics monitoring',
}
),
[DataTypes.SYNTHETICS]: i18n.translate('xpack.exploratoryView.syntheticsLabel', {
defaultMessage: 'Synthetics monitoring',
}),
[DataTypes.UPTIME]: i18n.translate('xpack.exploratoryView.overview.exploratoryView.uptimeLabel', {
[DataTypes.UPTIME]: i18n.translate('xpack.exploratoryView.uptimeLabel', {
defaultMessage: 'Uptime',
}),
[DataTypes.METRICS]: i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.metricsLabel',
{
defaultMessage: 'Metrics',
}
),
[DataTypes.METRICS]: i18n.translate('xpack.exploratoryView.metricsLabel', {
defaultMessage: 'Metrics',
}),
[DataTypes.LOGS]: i18n.translate('xpack.exploratoryView.overview.exploratoryView.logsLabel', {
[DataTypes.LOGS]: i18n.translate('xpack.exploratoryView.logsLabel', {
defaultMessage: 'Logs',
}),
[DataTypes.MOBILE]: i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.mobileExperienceLabel',
{
defaultMessage: 'Mobile experience',
}
),
[DataTypes.ALERTS]: i18n.translate('xpack.exploratoryView.overview.exploratoryView.alertsLabel', {
[DataTypes.MOBILE]: i18n.translate('xpack.exploratoryView.mobileExperienceLabel', {
defaultMessage: 'Mobile experience',
}),
[DataTypes.ALERTS]: i18n.translate('xpack.exploratoryView.alertsLabel', {
defaultMessage: 'Alerts',
}),
};

View file

@ -74,7 +74,7 @@ export function LensEmbeddable(props: Props) {
});
} else {
notifications?.toasts.add(
i18n.translate('xpack.exploratoryView.exploratoryView.noBrusing', {
i18n.translate('xpack.exploratoryView.noBrushing', {
defaultMessage: 'Zoom by brush selection is only available on time series charts.',
})
);

View file

@ -140,21 +140,18 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
}
export const NO_BREAK_DOWN_LABEL = i18n.translate(
'xpack.exploratoryView.exp.breakDownFilter.noBreakdown',
'xpack.exploratoryView.breakDownFilter.noBreakdown',
{
defaultMessage: 'No breakdown',
}
);
export const BREAKDOWN_WARNING = i18n.translate(
'xpack.exploratoryView.exp.breakDownFilter.warning',
{
defaultMessage: 'Breakdowns can be applied to only one series at a time.',
}
);
export const BREAKDOWN_WARNING = i18n.translate('xpack.exploratoryView.breakDownFilter.warning', {
defaultMessage: 'Breakdowns can be applied to only one series at a time.',
});
export const BREAKDOWN_UNAVAILABLE = i18n.translate(
'xpack.exploratoryView.exp.breakDownFilter.unavailable',
'xpack.exploratoryView.breakDownFilter.unavailable',
{
defaultMessage:
'Step name breakdown is not available for monitor duration metric. Use step duration metric to breakdown by step name.',

View file

@ -98,16 +98,10 @@ export function DataTypesSelect({ seriesId, series }: Props) {
);
}
const SELECT_DATA_TYPE_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.selectDataType',
{
defaultMessage: 'Select data type',
}
);
const SELECT_DATA_TYPE_LABEL = i18n.translate('xpack.exploratoryView.selectDataType', {
defaultMessage: 'Select data type',
});
const SELECT_DATA_TYPE_TOOLTIP = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.selectDataTypeTooltip',
{
defaultMessage: 'Data type cannot be edited.',
}
);
const SELECT_DATA_TYPE_TOOLTIP = i18n.translate('xpack.exploratoryView.selectDataTypeTooltip', {
defaultMessage: 'Data type cannot be edited.',
});

View file

@ -50,7 +50,7 @@ export function DatePickerCol({ seriesId, series }: Props) {
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<strong>
{i18n.translate('xpack.exploratoryView.overview.exploratoryView.noDataAvailable', {
{i18n.translate('xpack.exploratoryView.noDataAvailable', {
defaultMessage: 'No {dataType} data available.',
values: {
dataType: series.dataType,

View file

@ -29,7 +29,7 @@ export function IncompleteBadge({ seriesConfig, series }: Props) {
(!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading;
const incompleteDefinition = isEmpty(reportDefinitions)
? i18n.translate('xpack.exploratoryView.overview.exploratoryView.missingReportDefinition', {
? i18n.translate('xpack.exploratoryView.missingReportDefinition', {
defaultMessage: 'Missing {reportDefinition}',
values: {
reportDefinition:
@ -55,16 +55,10 @@ export function IncompleteBadge({ seriesConfig, series }: Props) {
return <EuiBadge color="warning">{incompleteMessage}</EuiBadge>;
}
const MISSING_REPORT_METRIC_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.missingReportMetric',
{
defaultMessage: 'Missing report metric',
}
);
const MISSING_REPORT_METRIC_LABEL = i18n.translate('xpack.exploratoryView.missingReportMetric', {
defaultMessage: 'Missing report metric',
});
const MISSING_DATA_TYPE_LABEL = i18n.translate(
'xpack.exploratoryView.overview.exploratoryView.missingDataType',
{
defaultMessage: 'Missing data type',
}
);
const MISSING_DATA_TYPE_LABEL = i18n.translate('xpack.exploratoryView.missingDataType', {
defaultMessage: 'Missing data type',
});

View file

@ -14,6 +14,7 @@
"data",
"dataViews",
"embeddable",
"exploratoryView",
"features",
"files",
"guidedOnboarding",

View file

@ -8,7 +8,8 @@
import { i18n } from '@kbn/i18n';
import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { CoreStart } from '@kbn/core/public';
import { AllSeries } from '@kbn/exploratory-view-plugin/public';
import { SERVICE_NAME, TRANSACTION_DURATION } from '@kbn/observability-shared-plugin/common';
import { UX_APP } from '../../../../context/constants';
import { ObservabilityPublicPluginsStart } from '../../../..';
import { SectionContainer } from '..';
@ -17,12 +18,7 @@ import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useHasData } from '../../../../hooks/use_has_data';
import { useDatePickerContext } from '../../../../hooks/use_date_picker_context';
import CoreVitals from '../../../shared/core_web_vitals';
import { getExploratoryViewEmbeddable } from '../../../shared/exploratory_view/embeddable';
import { AllSeries } from '../../../shared/exploratory_view/hooks/use_series_storage';
import {
SERVICE_NAME,
TRANSACTION_DURATION,
} from '../../../shared/exploratory_view/configurations/constants/elasticsearch_fieldnames';
import type { BucketSize } from '../../../../pages/overview/helpers/calculate_bucket_size';
interface Props {
bucketSize: BucketSize;
@ -31,15 +27,14 @@ interface Props {
export function UXSection({ bucketSize }: Props) {
const { forceUpdate, hasDataMap } = useHasData();
const { services } = useKibana<ObservabilityPublicPluginsStart>();
const { ExploratoryViewEmbeddable } = services.exploratoryView;
const { relativeStart, relativeEnd, absoluteStart, absoluteEnd, lastUpdated } =
useDatePickerContext();
const uxHasDataResponse = hasDataMap.ux;
const serviceName = uxHasDataResponse?.serviceName as string;
const ExploratoryViewEmbeddable = getExploratoryViewEmbeddable(
services as ObservabilityPublicPluginsStart & CoreStart
);
const seriesList: AllSeries = [
{
name: PAGE_LOAD_DISTRIBUTION_TITLE,

View file

@ -1,161 +0,0 @@
# Exploratory view component
This component is used in observability plugin to show lens embeddable based observability visualizations.
The view is populated using configs stored as json within the view for each data type.
This readme file contains few of the concepts being used in the component.
Basic workflow for how exploratory view works, it looks like this
![Exploratory view workflow](https://i.imgur.com/Kgyfd29.png)
## Report Type
The exploratory view report type controls how the data is visualized in the lens embeddable. The report type defines a set of constraints over the x and y axis. For example, the `kpi-over-time` report type is a time series chart type that plots key performance indicators over time, while the `data-distribution` chart plots the percentage of documents over key performance indicators. Current available data types can be found at `exploratory_view/configurations/constants`.
Each report type has one or more available visualizations to plot data from one or more data types.
## Data Types
Each available visualization is backed by a data type. A data type consists of a set of configuration for displaying domain-specific visualizations for observability data. Some example data types include apm, metrics, and logs.
For each respective data type, we fetch index pattern string from the app plugin contract, leveraging existing hasData API we have to return the index pattern string as well as a `hasData` boolean from each plugin.
In most cases, there will be a 1-1 relation between apps and data types.
### Observability `dataViews`
Once we have index pattern string for each data type, a respective `dataView` is created. If there is an existing dataView for an index pattern, we will fetch and reuse it.
After the dataView is created we also set field formats to promote human-readability. For example, we set format for monitor duration field, which is monitor.duration.us, from microseconds to seconds for browser monitors.
### Visualization Configuration
Each data type may have one or more visualization configurations. The data type to visualization configuration can be found in [`exploratory_view/obs_exploratory_view`](https://github.com/elastic/kibana/blob/main/x-pack/plugins/observability/public/components/shared/exploratory_view/obsv_exploratory_view.tsx#L86)
Each visualization configuration is mapped to a single report type.
Visualization configurations are used to define the UI we display for each report type and data type combination in the series builder.
Visualization configuration define UI options and display, including available `metrics`, available `filters`, available `breakdown` options, definitions for human-readable `labels`, and more.
The configuration also defines any custom base filters, which usually get pushed to a query, but are not displayed on the UI. You can also set more custom options on the configuration like colors which get used while rendering the chart.
Visualization configuration can be found at [`exploratory_view/configurations`](https://github.com/elastic/kibana/tree/main/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations), where each data type typically has a folder that holds one or more visualization configurations.
The configuration defined ultimately influences the lens embeddable attributes which get pushed to lens embeddable, rendering the chart.
Some options in configuration are:
#### Definition fields
They are also filters, but usually main filters, around which usually app UI is based.
For apm, it could be service name and for uptime, monitor name.
#### Filters
You can define base filters in kql form or data plugin filter format, filters are strongly typed.
#### Breakdown fields
List of fields from an index pattern, UI will use this to populate breakdown option select.
#### Labels
You can set key/value map for your field labels. UI will use these to set labels for data view fields.
Sample config
```
{
reportType: ReportTypes.KPI,
defaultSeriesType: 'bar_stacked',
xAxisColumn: {
sourceField: '@timestamp',
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'median',
},
],
hasOperationType: false,
filterFields: ['observer.geo.name', 'monitor.type', 'tags'], // these fields get's resolved from relevant dataView
breakdownFields: [
'observer.geo.name',
'monitor.type',
'monitor.name',
PERCENTILE,
], // these fields get's resolved from relevant dataView
baseFilters: [],
palette: { type: 'palette', name: 'status' },
definitionFields: [
{ field: 'monitor.name', nested: SYNTHETICS_STEP_NAME, singleSelection: true },
{ field: 'url.full', filters: buildExistsFilter('summary.up', dataView) },
],
metricOptions: [
{
label: MONITORS_DURATION_LABEL,
field: 'monitor.duration.us',
columnType: OPERATION_COLUMN,
}
],
labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL },
}
```
## Lens Embeddable
Lens embeddable is what actually renders the chart in exploratory view.
Exploratory view generates the lens embeddable attributes as json and pass it to the component.
Based on configuration, exploratory view generates layers and columns.
Add a link to lens embeddable readme
#### Example
A simple usage of lens embeddable example and playground options
[embedded_lens_example](../../../../../../examples/embedded_lens_example)
## Exploratory view Embeddable
The primary purpose of the exploratory view is to embed it in observability solutions like uptime to replace
existing static visualizations,
For that purpose, all the configuration options we define in the exploratory view can be used as an embeddable
via a component that is exposed using observability plugin contract,
usage looks like this
`const ExploratoryViewComponent = props.plugins.observability.ExploratoryViewEmbeddable;
`
```
<ExploratoryViewComponent
attributes={[
{
name: 'Monitors response duration',
time: {
from: 'now-5d',
to: 'now',
},
reportDefinitions: {
'monitor.id': ['test-id'],
},
breakdown: 'monitor.type',
operationType: 'average',
dataType: 'synthetics',
seriesType: 'line',
selectedMetricField: 'monitor.duration.us',
},
]}
reportType="kpi-over-time"
title={'Monitor response duration'}
withActions={['save', 'explore']}
/>
```
there is an example in kibana example which you can view using
`yarn start --run-examples` and view the code at [Exploratory view embeddable](../../../../../../examples/exploratory_view_example)
#### Example
A simple usage of lens embeddable example and playground options, run kibana with
`yarn start --run-example` to see this example in action
source code is defined at [embedded_lens_example](../../../../../../examples/embedded_lens_example)

View file

@ -1,70 +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 { render } from '../../rtl_helpers';
import { fireEvent, screen } from '@testing-library/dom';
import React from 'react';
import { sampleAttribute } from '../../configurations/test_data/sample_attribute';
import * as pluginHook from '../../../../../hooks/use_plugin_context';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { ExpViewActionMenuContent } from './action_menu';
import { noCasesPermissions as mockUseGetCasesPermissions } from '../../../../../utils/cases_permissions';
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
appMountParameters: {
setHeaderActionMenu: jest.fn(),
},
} as any);
jest.mock('../../../../../hooks/use_get_user_cases_permissions', () => ({
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
}));
describe('Action Menu', function () {
afterAll(() => {
jest.clearAllMocks();
});
it('should be able to click open in lens', async function () {
const { findByText, core } = render(
<ExpViewActionMenuContent
lensAttributes={sampleAttribute as TypedLensByValueInput['attributes']}
timeRange={{ to: 'now', from: 'now-10m' }}
/>
);
expect(await screen.findByText('Open in Lens')).toBeInTheDocument();
fireEvent.click(await findByText('Open in Lens'));
expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith(
{
id: '',
attributes: sampleAttribute,
timeRange: { to: 'now', from: 'now-10m' },
},
{
openInNewTab: true,
}
);
});
it('should be able to click save', async function () {
const { findByText } = render(
<ExpViewActionMenuContent
lensAttributes={sampleAttribute as TypedLensByValueInput['attributes']}
/>
);
expect(await screen.findByText('Save')).toBeInTheDocument();
fireEvent.click(await findByText('Save'));
expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument();
});
});

View file

@ -1,109 +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 React, { useState } from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { LensEmbeddableInput, TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { EmbedAction } from '../../header/embed_action';
import { ObservabilityAppServices } from '../../../../../application/types';
import { AddToCaseAction } from '../../header/add_to_case_action';
export function ExpViewActionMenuContent({
timeRange,
lensAttributes,
}: {
timeRange?: { from: string; to: string };
lensAttributes: TypedLensByValueInput['attributes'] | null;
}) {
const kServices = useKibana<ObservabilityAppServices>().services;
const { lens, isDev } = kServices;
const [isSaveOpen, setIsSaveOpen] = useState(false);
const LensSaveModalComponent = lens.SaveModalComponent;
return (
<>
<EuiFlexGroup
alignItems="center"
gutterSize="s"
responsive={false}
style={{ paddingRight: 20 }}
>
{isDev && (
<EuiFlexItem grow={false}>
<EmbedAction lensAttributes={lensAttributes} />
</EuiFlexItem>
)}
{timeRange && (
<EuiFlexItem grow={false}>
<AddToCaseAction lensAttributes={lensAttributes} timeRange={timeRange} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="o11yExpViewActionMenuContentOpenInLensButton"
iconType="lensApp"
fullWidth={false}
isDisabled={!lens.canUseEditor() || lensAttributes === null}
size="s"
onClick={() => {
if (lensAttributes) {
lens.navigateToPrefilledEditor(
{
id: '',
timeRange,
attributes: lensAttributes,
},
{
openInNewTab: true,
}
);
}
}}
>
{i18n.translate('xpack.observability.expView.heading.openInLens', {
defaultMessage: 'Open in Lens',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="o11yExpViewActionMenuContentSaveButton"
fill={true}
iconType="save"
fullWidth={false}
isDisabled={!lens.canUseEditor() || lensAttributes === null}
onClick={() => {
if (lensAttributes) {
setIsSaveOpen(true);
}
}}
size="s"
>
{i18n.translate('xpack.observability.expView.heading.saveLensVisualization', {
defaultMessage: 'Save',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
{isSaveOpen && lensAttributes && (
<LensSaveModalComponent
initialInput={lensAttributes as unknown as LensEmbeddableInput}
onClose={() => setIsSaveOpen(false)}
// if we want to do anything after the viz is saved
// right now there is no action, so an empty function
onSave={() => {}}
/>
)}
</>
);
}

View file

@ -1,26 +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 React from 'react';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { ExpViewActionMenuContent } from './action_menu';
import HeaderMenuPortal from '../../../header_menu_portal';
import { useExploratoryView } from '../../contexts/exploratory_view_config';
interface Props {
timeRange?: { from: string; to: string };
lensAttributes: TypedLensByValueInput['attributes'] | null;
}
export function ExpViewActionMenu(props: Props) {
const { setHeaderActionMenu, theme$ } = useExploratoryView();
return (
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
<ExpViewActionMenuContent {...props} />
</HeaderMenuPortal>
);
}

View file

@ -1,119 +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 React from 'react';
import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui';
import { Moment } from 'moment';
import DateMath from '@kbn/datemath';
import { i18n } from '@kbn/i18n';
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
import { useSeriesStorage } from '../hooks/use_series_storage';
import { SeriesUrl } from '../types';
import { ReportTypes } from '../configurations/constants';
export const parseRelativeDate = (date: string, options = {}): Moment | void => {
return DateMath.parse(date, options)!;
};
export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) {
const { firstSeries, setSeries, reportType } = useSeriesStorage();
const dateFormat = useUiSetting<string>('dateFormat');
const seriesFrom = series.time?.from;
const seriesTo = series.time?.to;
const { from: mainFrom, to: mainTo } = firstSeries!.time;
const startDate = parseRelativeDate(seriesFrom ?? mainFrom)!;
const endDate = parseRelativeDate(seriesTo ?? mainTo, { roundUp: true })!;
const getTotalDuration = () => {
const mainStartDate = parseRelativeDate(mainFrom)!;
const mainEndDate = parseRelativeDate(mainTo, { roundUp: true })!;
return mainEndDate.diff(mainStartDate, 'millisecond');
};
const onStartChange = (newStartDate: Moment) => {
if (reportType === ReportTypes.KPI) {
const totalDuration = getTotalDuration();
const newFrom = newStartDate.toISOString();
const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString();
setSeries(seriesId, {
...series,
time: { from: newFrom, to: newTo },
});
} else {
const newFrom = newStartDate.toISOString();
setSeries(seriesId, {
...series,
time: { from: newFrom, to: seriesTo },
});
}
};
const onEndChange = (newEndDate: Moment) => {
if (reportType === ReportTypes.KPI) {
const totalDuration = getTotalDuration();
const newTo = newEndDate.toISOString();
const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString();
setSeries(seriesId, {
...series,
time: { from: newFrom, to: newTo },
});
} else {
const newTo = newEndDate.toISOString();
setSeries(seriesId, {
...series,
time: { from: seriesFrom, to: newTo },
});
}
};
return (
<EuiDatePickerRange
fullWidth
isCustom
startDateControl={
<EuiDatePicker
fullWidth
selected={startDate}
onChange={onStartChange}
startDate={startDate}
endDate={endDate}
isInvalid={startDate > endDate}
aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', {
defaultMessage: 'Start date',
})}
dateFormat={dateFormat.replace('ss.SSS', 'ss')}
showTimeSelect
popoverPlacement="rightCenter"
/>
}
endDateControl={
<EuiDatePicker
fullWidth
showIcon={false}
selected={endDate}
onChange={onEndChange}
startDate={startDate}
endDate={endDate}
isInvalid={startDate > endDate}
aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', {
defaultMessage: 'End date',
})}
dateFormat={dateFormat.replace('ss.SSS', 'ss')}
showTimeSelect
popoverPlacement="rightCenter"
/>
}
/>
);
}

View file

@ -1,99 +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 React from 'react';
import { isEmpty } from 'lodash';
import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { LOADING_VIEW } from '../series_editor/series_editor';
import { ReportViewType, SeriesUrl } from '../types';
export function EmptyView({
loading,
series,
reportType,
}: {
loading: boolean;
series?: SeriesUrl;
reportType: ReportViewType;
}) {
const { dataType, reportDefinitions } = series ?? {};
let emptyMessage = EMPTY_LABEL;
if (dataType) {
if (reportType) {
if (isEmpty(reportDefinitions)) {
emptyMessage = CHOOSE_REPORT_DEFINITION;
}
} else {
emptyMessage = SELECT_REPORT_TYPE_BELOW;
}
} else {
emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT;
}
if (!series) {
emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
defaultMessage: 'No series found. Please add a series.',
});
}
return (
<Wrapper>
{loading && (
<EuiProgress
size="xs"
color="accent"
position="absolute"
style={{
top: 'initial',
}}
/>
)}
<EuiSpacer />
<FlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem>
<EuiText>{loading ? LOADING_VIEW : emptyMessage}</EuiText>
</EuiFlexItem>
</FlexGroup>
</Wrapper>
);
}
const Wrapper = styled.div`
text-align: center;
position: relative;
`;
const FlexGroup = styled(EuiFlexGroup)`
height: 100%;
`;
export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.emptyview', {
defaultMessage: 'Nothing to display.',
});
export const CHOOSE_REPORT_DEFINITION = i18n.translate(
'xpack.observability.expView.seriesBuilder.emptyReportDefinition',
{
defaultMessage: 'Select a report definition to create a visualization.',
}
);
export const SELECT_REPORT_TYPE_BELOW = i18n.translate(
'xpack.observability.expView.seriesBuilder.selectReportType.empty',
{
defaultMessage: 'Select a report type to create a visualization.',
}
);
const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate(
'xpack.observability.expView.reportType.selectDataType',
{ defaultMessage: 'Select a data type to create a visualization.' }
);

View file

@ -1,146 +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 React from 'react';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { mockAppDataView, mockDataView, mockUxSeries, render } from '../rtl_helpers';
import { FilterLabel } from './filter_label';
import * as useSeriesHook from '../hooks/use_series_filters';
import { buildFilterLabel } from '../../filter_value_label/filter_value_label';
import 'jest-canvas-mock';
jest.setTimeout(10 * 1000);
describe('FilterLabel', function () {
mockAppDataView();
const invertFilter = jest.fn();
jest.spyOn(useSeriesHook, 'useSeriesFilters').mockReturnValue({
invertFilter,
} as any);
it('should render properly', async function () {
render(
<FilterLabel
field={'service.name'}
value={'elastic-co'}
label={'Web Application'}
negate={false}
seriesId={0}
removeFilter={jest.fn()}
dataView={mockDataView}
series={mockUxSeries}
/>
);
await waitFor(async () => {
expect(await screen.findByText('elastic-co')).toBeInTheDocument();
expect(await screen.findByText('elastic-co')).toBeInTheDocument();
expect(await screen.findByText(/web application:/i)).toBeInTheDocument();
expect(await screen.findByTitle('Delete Web Application: elastic-co')).toBeInTheDocument();
});
});
it('should delete filter', async function () {
const removeFilter = jest.fn();
render(
<FilterLabel
field={'service.name'}
value={'elastic-co'}
label={'Web Application'}
negate={false}
seriesId={0}
removeFilter={removeFilter}
dataView={mockDataView}
series={mockUxSeries}
/>
);
fireEvent.click(await screen.findByLabelText('Filter actions'));
fireEvent.click(await screen.findByTestId('deleteFilter'));
expect(removeFilter).toHaveBeenCalledTimes(1);
expect(removeFilter).toHaveBeenCalledWith('service.name', 'elastic-co', false);
});
it('should invert filter', async function () {
const removeFilter = jest.fn();
render(
<FilterLabel
field={'service.name'}
value={'elastic-co'}
label={'Web Application'}
negate={false}
seriesId={0}
removeFilter={removeFilter}
dataView={mockDataView}
series={mockUxSeries}
/>
);
fireEvent.click(await screen.findByLabelText('Filter actions'));
fireEvent.click(await screen.findByTestId('negateFilter'));
expect(invertFilter).toHaveBeenCalledTimes(1);
expect(invertFilter).toHaveBeenCalledWith({
field: 'service.name',
negate: false,
value: 'elastic-co',
});
});
it('should display invert filter', async function () {
render(
<FilterLabel
field={'service.name'}
value={'elastic-co'}
label={'Web Application'}
negate={true}
seriesId={0}
removeFilter={jest.fn()}
dataView={mockDataView}
series={mockUxSeries}
/>
);
expect(await screen.findByText('elastic-co')).toBeInTheDocument();
expect(await screen.findByText(/web application:/i)).toBeInTheDocument();
expect(await screen.findByTitle('Delete NOT Web Application: elastic-co')).toBeInTheDocument();
expect(
await screen.findByRole('button', {
name: /delete not web application: elastic-co/i,
})
).toBeInTheDocument();
});
it('should build filter meta', function () {
expect(
buildFilterLabel({
field: 'user_agent.name',
label: 'Browser family',
dataView: mockDataView,
value: 'Firefox',
negate: false,
})
).toEqual({
meta: {
alias: null,
disabled: false,
index: 'apm-*',
key: 'Browser family',
negate: false,
type: 'phrase',
value: 'Firefox',
},
query: {
match_phrase: {
'user_agent.name': 'Firefox',
},
},
});
});
});

View file

@ -1,52 +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 React from 'react';
import type { DataView } from '@kbn/data-views-plugin/common';
import { useSeriesFilters } from '../hooks/use_series_filters';
import { FilterValueLabel } from '../../filter_value_label/filter_value_label';
import { SeriesUrl } from '../types';
interface Props {
field: string;
label: string;
value: string | Array<string | number>;
seriesId: number;
series: SeriesUrl;
negate: boolean;
definitionFilter?: boolean;
dataView: DataView;
removeFilter: (field: string, value: string | Array<string | number>, notVal: boolean) => void;
}
export function FilterLabel({
label,
seriesId,
series,
field,
value,
negate,
dataView,
removeFilter,
definitionFilter,
}: Props) {
const { invertFilter } = useSeriesFilters({ seriesId, series });
return dataView ? (
<FilterValueLabel
dataView={dataView}
removeFilter={removeFilter}
invertFilter={(val) => {
if (!definitionFilter) invertFilter(val);
}}
field={field}
value={value}
negate={negate}
label={label}
/>
) : null;
}

View file

@ -1,70 +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 React, { useState } from 'react';
import {
EuiColorPicker,
EuiFormRow,
EuiIcon,
EuiPopover,
EuiToolTip,
EuiButtonEmpty,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useTheme } from '../../../../hooks/use_theme';
import { useSeriesStorage } from '../hooks/use_series_storage';
import { SeriesUrl } from '../types';
export function SeriesColorPicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) {
const theme = useTheme();
const { setSeries } = useSeriesStorage();
const [isOpen, setIsOpen] = useState(false);
const onChange = (colorN: string) => {
setSeries(seriesId, { ...series, color: colorN });
};
const color =
series.color ?? (theme.eui as unknown as Record<string, string>)[`euiColorVis${seriesId}`];
const button = (
<EuiToolTip content={EDIT_SERIES_COLOR_LABEL}>
<EuiButtonEmpty
data-test-subj="o11ySeriesColorPickerButton"
size="s"
onClick={() => setIsOpen((prevState) => !prevState)}
flush="both"
>
<EuiIcon type="stopFilled" size="l" color={color} />
</EuiButtonEmpty>
</EuiToolTip>
);
return (
<EuiPopover button={button} isOpen={isOpen} closePopover={() => setIsOpen(false)}>
<EuiFormRow label={PICK_A_COLOR_LABEL}>
<EuiColorPicker onChange={onChange} color={color} />
</EuiFormRow>
</EuiPopover>
);
}
const PICK_A_COLOR_LABEL = i18n.translate(
'xpack.observability.overview.exploratoryView.pickColor',
{
defaultMessage: 'Pick a color',
}
);
const EDIT_SERIES_COLOR_LABEL = i18n.translate(
'xpack.observability.overview.exploratoryView.editSeriesColor',
{
defaultMessage: 'Edit color for series',
}
);

View file

@ -1,59 +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 { EuiSuperDatePicker } from '@elastic/eui';
import React from 'react';
import { useHasData } from '../../../../../hooks/use_has_data';
import { useSeriesStorage } from '../../hooks/use_series_storage';
import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges';
import { SeriesUrl } from '../../types';
import { ReportTypes } from '../../configurations/constants';
export interface TimePickerTime {
from: string;
to: string;
}
export interface TimePickerQuickRange extends TimePickerTime {
display: string;
}
interface Props {
seriesId: number;
series: SeriesUrl;
}
export function SeriesDatePicker({ series, seriesId }: Props) {
const { onRefreshTimeRange } = useHasData();
const commonlyUsedRanges = useQuickTimeRanges();
const { setSeries, reportType, allSeries } = useSeriesStorage();
function onTimeChange({ start, end }: { start: string; end: string }) {
onRefreshTimeRange?.();
if (reportType === ReportTypes.KPI) {
allSeries.forEach((currSeries, seriesIndex) => {
setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } });
});
} else {
setSeries(seriesId, { ...series, time: { from: start, to: end } });
}
}
return (
<EuiSuperDatePicker
start={series?.time?.from}
end={series?.time?.to}
onTimeChange={onTimeChange}
commonlyUsedRanges={commonlyUsedRanges}
onRefresh={onTimeChange}
showUpdateButton={false}
/>
);
}

View file

@ -1,68 +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 React from 'react';
import { mockUseHasData, render } from '../../rtl_helpers';
import { fireEvent, waitFor } from '@testing-library/react';
import { SeriesDatePicker } from '.';
describe('SeriesDatePicker', function () {
it('should render properly', function () {
const initSeries = {
data: [
{
name: 'uptime-pings-histogram',
dataType: 'synthetics' as const,
breakdown: 'monitor.status',
time: { from: 'now-30m', to: 'now' },
},
],
};
const { getByText } = render(<SeriesDatePicker seriesId={0} series={initSeries.data[0]} />, {
initSeries,
});
getByText('Last 30 minutes');
});
it('should set series data', async function () {
const initSeries = {
data: [
{
name: 'uptime-pings-histogram',
dataType: 'synthetics' as const,
breakdown: 'monitor.status',
time: { from: 'now-30m', to: 'now' },
},
],
};
const { onRefreshTimeRange } = mockUseHasData();
const { getByTestId, setSeries } = render(
<SeriesDatePicker seriesId={0} series={initSeries.data[0]} />,
{
initSeries,
}
);
await waitFor(function () {
fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton'));
});
fireEvent.click(getByTestId('superDatePickerCommonlyUsed_Today'));
expect(onRefreshTimeRange).toHaveBeenCalledTimes(1);
expect(setSeries).toHaveBeenCalledWith(0, {
name: 'uptime-pings-histogram',
breakdown: 'monitor.status',
dataType: 'synthetics',
time: { from: 'now/d', to: 'now/d' },
});
expect(setSeries).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,111 +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 React, { useState } from 'react';
import { fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import { createMemoryHistory } from 'history';
import * as fetcherHook from '../../../../../hooks/use_fetcher';
import { SelectableUrlList } from './selectable_url_list';
import { I18LABELS } from './translations';
import { render } from '../../rtl_helpers';
describe('SelectableUrlList', () => {
jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
data: {},
status: fetcherHook.FETCH_STATUS.SUCCESS,
refetch: jest.fn(),
});
const customHistory = createMemoryHistory({
initialEntries: ['/?searchTerm=blog'],
});
function WrappedComponent() {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
return (
<SelectableUrlList
initialValue={'blog'}
loading={false}
data={{ items: [], total: 0 }}
onSelectionChange={jest.fn()}
searchValue={'blog'}
onInputChange={jest.fn()}
popoverIsOpen={Boolean(isPopoverOpen)}
setPopoverIsOpen={setIsPopoverOpen}
onSelectionApply={jest.fn()}
hasChanged={() => true}
/>
);
}
it('it uses search term value from url', () => {
const { getByDisplayValue } = render(
<SelectableUrlList
initialValue={'blog'}
loading={false}
data={{ items: [], total: 0 }}
onSelectionChange={jest.fn()}
searchValue={'blog'}
onInputChange={jest.fn()}
popoverIsOpen={false}
setPopoverIsOpen={jest.fn()}
onSelectionApply={jest.fn()}
hasChanged={() => true}
/>,
{ history: customHistory }
);
expect(getByDisplayValue('blog')).toBeInTheDocument();
});
it('maintains focus on search input field', () => {
const { getByLabelText } = render(
<SelectableUrlList
initialValue={'blog'}
loading={false}
data={{ items: [], total: 0 }}
onSelectionChange={jest.fn()}
searchValue={'blog'}
onInputChange={jest.fn()}
popoverIsOpen={false}
setPopoverIsOpen={jest.fn()}
onSelectionApply={jest.fn()}
hasChanged={() => true}
/>,
{ history: customHistory }
);
const input = getByLabelText(I18LABELS.filterByUrl);
fireEvent.click(input);
expect(document.activeElement).toBe(input);
});
it('hides popover on escape', async () => {
const { getByText, getByLabelText, queryByText } = render(<WrappedComponent />, {
history: customHistory,
});
const input = getByLabelText(I18LABELS.filterByUrl);
fireEvent.click(input);
// wait for title of popover to be present
await waitFor(() => {
expect(getByText(I18LABELS.getSearchResultsLabel(0))).toBeInTheDocument();
});
// escape key
fireEvent.keyDown(input, {
key: 'Escape',
code: 'Escape',
keyCode: 27,
charCode: 27,
});
// wait for title of popover to be removed
await waitForElementToBeRemoved(() => queryByText(I18LABELS.getSearchResultsLabel(0)));
});
});

View file

@ -1,226 +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 React, {
SetStateAction,
useRef,
useState,
KeyboardEvent,
ReactNode,
FormEventHandler,
} from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiLoadingSpinner,
EuiPopover,
EuiPopoverTitle,
EuiSelectable,
EuiSelectableMessage,
EuiPopoverFooter,
EuiButton,
EuiButtonIcon,
EuiSelectableOption,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import useEvent from 'react-use/lib/useEvent';
import classNames from 'classnames';
import { I18LABELS } from './translations';
export type UrlOption<T = { [key: string]: any }> = {
meta?: string[];
isNewWildcard?: boolean;
isWildcard?: boolean;
title: string;
} & EuiSelectableOption<T>;
export interface SelectableUrlListProps {
data: {
items: UrlOption[];
total?: number;
};
loading: boolean;
rowHeight?: number;
onInputChange: (val: string) => void;
onSelectionApply: () => void;
onSelectionChange: (updatedOptions: UrlOption[]) => void;
searchValue: string;
popoverIsOpen: boolean;
initialValue?: string;
setPopoverIsOpen: React.Dispatch<SetStateAction<boolean>>;
renderOption?: (option: UrlOption, searchValue: string) => ReactNode;
hasChanged: () => boolean;
}
export const formatOptions = (options: EuiSelectableOption[]) => {
return options.map((item: EuiSelectableOption) => ({
title: item.label,
...item,
className: classNames('euiSelectableTemplateSitewide__listItem', item.className),
}));
};
export function SelectableUrlList({
data,
loading,
onInputChange,
onSelectionChange,
onSelectionApply,
searchValue,
popoverIsOpen,
setPopoverIsOpen,
initialValue,
renderOption,
rowHeight,
hasChanged,
}: SelectableUrlListProps) {
const [searchRef, setSearchRef] = useState<HTMLInputElement | null>(null);
const titleRef = useRef<HTMLDivElement>(null);
const formattedOptions = formatOptions(data.items ?? []);
const onEnterKey = (evt: KeyboardEvent<HTMLInputElement>) => {
if (evt.key.toLowerCase() === 'enter') {
onSelectionApply();
setPopoverIsOpen(false);
}
};
const onInputClick = (e: React.MouseEvent<HTMLInputElement>) => {
setPopoverIsOpen(true);
if (searchRef) {
searchRef.focus();
}
};
const onSearchInput: FormEventHandler<HTMLInputElement> = (e) => {
onInputChange((e.target as HTMLInputElement).value);
setPopoverIsOpen(true);
};
const closePopover = () => {
setPopoverIsOpen(false);
};
// @ts-ignore - not sure, why it's not working
useEvent('keydown', onEnterKey, searchRef);
useEvent('escape', () => setPopoverIsOpen(false), searchRef);
const loadingMessage = (
<EuiSelectableMessage style={{ minHeight: 300 }}>
<EuiLoadingSpinner size="l" />
<br />
<p>{I18LABELS.loadingResults}</p>
</EuiSelectableMessage>
);
const emptyMessage = (
<EuiSelectableMessage style={{ minHeight: 300 }}>
<p>{I18LABELS.noResults}</p>
</EuiSelectableMessage>
);
const titleText = searchValue
? I18LABELS.getSearchResultsLabel(data?.total ?? 0)
: I18LABELS.topPages;
function PopOverTitle() {
return (
<EuiPopoverTitle paddingSize="s">
<EuiFlexGroup ref={titleRef} gutterSize="xs">
<EuiFlexItem style={{ justifyContent: 'center' }}>
{loading ? <EuiLoadingSpinner /> : titleText}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="text"
onClick={() => closePopover()}
aria-label={i18n.translate('xpack.observability.search.url.close', {
defaultMessage: 'Close',
})}
iconType={'cross'}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>
);
}
return (
<EuiSelectable
searchable
onChange={onSelectionChange}
options={searchValue !== searchRef?.value ? [] : formattedOptions}
renderOption={renderOption}
singleSelection={false}
searchProps={{
value: searchValue,
isClearable: true,
onClick: onInputClick,
onInput: onSearchInput,
inputRef: setSearchRef,
placeholder: I18LABELS.filterByUrl,
'aria-label': I18LABELS.filterByUrl,
}}
listProps={{
rowHeight,
showIcons: true,
onFocusBadge: false,
}}
loadingMessage={loadingMessage}
emptyMessage={emptyMessage}
noMatchesMessage={emptyMessage}
allowExclusions={true}
isPreFiltered={searchValue !== searchRef?.value}
>
{(list, search) => (
<EuiPopover
panelPaddingSize="none"
isOpen={popoverIsOpen}
display={'block'}
button={search}
closePopover={closePopover}
style={{ minWidth: 400 }}
anchorPosition="downLeft"
ownFocus={false}
>
<div
style={{
width: searchRef?.getBoundingClientRect().width ?? 600,
maxWidth: '100%',
}}
>
<PopOverTitle />
{list}
<EuiPopoverFooter paddingSize="s">
<EuiFlexGroup style={{ justifyContent: 'flex-end' }}>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="o11ySelectableUrlListApplyButton"
fill
size="s"
onClick={() => {
onSelectionApply();
closePopover();
}}
isDisabled={!hasChanged()}
>
{i18n.translate('xpack.observability.apply.label', {
defaultMessage: 'Apply',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverFooter>
</div>
</EuiPopover>
)}
</EuiSelectable>
);
}
// eslint-disable-next-line import/no-default-export
export default SelectableUrlList;

View file

@ -1,34 +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';
export const I18LABELS = {
filterByUrl: i18n.translate('xpack.observability.filters.filterByUrl', {
defaultMessage: 'Filter by URL',
}),
getSearchResultsLabel: (total: number) =>
i18n.translate('xpack.observability.filters.searchResults', {
defaultMessage: '{total} Search results',
values: { total },
}),
topPages: i18n.translate('xpack.observability.filters.topPages', {
defaultMessage: 'Top pages',
}),
select: i18n.translate('xpack.observability.filters.select', {
defaultMessage: 'Select',
}),
url: i18n.translate('xpack.observability.filters.url', {
defaultMessage: 'Url',
}),
loadingResults: i18n.translate('xpack.observability.filters.url.loadingResults', {
defaultMessage: 'Loading results',
}),
noResults: i18n.translate('xpack.observability.filters.url.noResults', {
defaultMessage: 'No results available',
}),
};

View file

@ -1,231 +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 React, { useEffect, useState } from 'react';
import { isEqual, map } from 'lodash';
import { i18n } from '@kbn/i18n';
import { SelectableUrlList, UrlOption } from './selectable_url_list';
import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types';
import { useUrlSearch } from './use_url_search';
import { useSeriesFilters } from '../../hooks/use_series_filters';
import { TRANSACTION_URL } from '../../configurations/constants/elasticsearch_fieldnames';
interface Props {
seriesId: number;
seriesConfig: SeriesConfig;
series: SeriesUrl;
}
const processSelectedItems = (items: UrlOption[]) => {
const urlItems = items.filter(({ isWildcard }) => !isWildcard);
const wildcardItems = items.filter(({ isWildcard }) => isWildcard);
const includedItems = map(
urlItems.filter((option) => option.checked === 'on'),
'label'
);
const excludedItems = map(
urlItems.filter((option) => option.checked === 'off'),
'label'
);
// for wild cards we use title since label contains extra information
const includedWildcards = map(
wildcardItems.filter((option) => option.checked === 'on'),
'title'
);
// for wild cards we use title since label contains extra information
const excludedWildcards = map(
wildcardItems.filter((option) => option.checked === 'off'),
'title'
);
return { includedItems, excludedItems, includedWildcards, excludedWildcards };
};
const getWildcardLabel = (wildcard: string) => {
return i18n.translate('xpack.observability.urlFilter.wildcard', {
defaultMessage: 'Use wildcard *{wildcard}*',
values: { wildcard },
});
};
export function URLSearch({ series, seriesConfig, seriesId }: Props) {
const [popoverIsOpen, setPopoverIsOpen] = useState(false);
const [query, setQuery] = useState('');
const [items, setItems] = useState<UrlOption[]>([]);
const { values, loading } = useUrlSearch({
query,
series,
seriesConfig,
seriesId,
});
useEffect(() => {
const queryLabel = getWildcardLabel(query);
const currFilter: UrlFilter | undefined = (series.filters ?? []).find(
({ field }) => field === TRANSACTION_URL
);
const {
wildcards = [],
notWildcards = [],
values: currValues = [],
notValues: currNotValues = [],
} = currFilter ?? { field: TRANSACTION_URL };
setItems((prevItems) => {
const { includedItems, excludedItems } = processSelectedItems(prevItems);
const newItems: UrlOption[] = (values ?? []).map((item) => {
if (
includedItems.includes(item.label) ||
wildcards.includes(item.label) ||
currValues.includes(item.label)
) {
return { ...item, checked: 'on', title: item.label };
}
if (
excludedItems.includes(item.label) ||
notWildcards.includes(item.label) ||
currNotValues.includes(item.label)
) {
return { ...item, checked: 'off', title: item.label, ...item };
}
return { ...item, title: item.label, checked: undefined };
});
wildcards.forEach((wildcard) => {
newItems.unshift({
title: wildcard,
label: getWildcardLabel(wildcard),
isWildcard: true,
checked: 'on',
});
});
notWildcards.forEach((wildcard) => {
newItems.unshift({
title: wildcard,
label: getWildcardLabel(wildcard),
isWildcard: true,
checked: 'off',
});
});
let queryItem: UrlOption | undefined = prevItems.find(({ isNewWildcard }) => isNewWildcard);
if (query) {
if (!queryItem) {
queryItem = {
title: query,
label: queryLabel,
isNewWildcard: true,
isWildcard: true,
};
newItems.unshift(queryItem);
}
return [{ ...queryItem, label: queryLabel, title: query }, ...newItems];
}
return newItems;
});
// we don't want to add series in the dependency, for that we have an extra side effect below
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [values, loading, query]);
useEffect(() => {
const currFilter: UrlFilter | undefined = (series.filters ?? []).find(
({ field }) => field === TRANSACTION_URL
);
const {
wildcards = [],
notWildcards = [],
values: currValues = [],
notValues: currNotValues = [],
} = currFilter ?? { field: TRANSACTION_URL };
setItems((prevItems) => {
const newItems: UrlOption[] = (prevItems ?? []).map((item) => {
if (currValues.includes(item.label) || wildcards.includes(item.title)) {
return { ...item, checked: 'on' };
}
if (currNotValues.includes(item.label) || notWildcards.includes(item.title)) {
return { ...item, checked: 'off' };
}
return { ...item, checked: undefined };
});
return newItems;
});
}, [series]);
const onSelectionChange = (updatedOptions: UrlOption[]) => {
setItems(updatedOptions);
};
const { replaceFilter } = useSeriesFilters({ seriesId, series });
const onSelectionApply = () => {
const { includedItems, excludedItems, includedWildcards, excludedWildcards } =
processSelectedItems(items);
replaceFilter({
field: TRANSACTION_URL,
values: includedItems,
notValues: excludedItems,
wildcards: includedWildcards,
notWildcards: excludedWildcards,
});
setQuery('');
setPopoverIsOpen(false);
};
const hasChanged = () => {
const { includedItems, excludedItems, includedWildcards, excludedWildcards } =
processSelectedItems(items);
const currFilter: UrlFilter | undefined = (series.filters ?? []).find(
({ field }) => field === TRANSACTION_URL
);
const {
wildcards = [],
notWildcards = [],
values: currValues = [],
notValues: currNotValues = [],
} = currFilter ?? { field: TRANSACTION_URL };
return (
!isEqual(includedItems.sort(), currValues.sort()) ||
!isEqual(excludedItems.sort(), currNotValues.sort()) ||
!isEqual(wildcards.sort(), includedWildcards.sort()) ||
!isEqual(notWildcards.sort(), excludedWildcards.sort())
);
};
return (
<SelectableUrlList
loading={Boolean(loading)}
onInputChange={(val) => setQuery(val)}
data={{ items, total: items.length }}
onSelectionChange={onSelectionChange}
searchValue={query}
popoverIsOpen={popoverIsOpen}
setPopoverIsOpen={setPopoverIsOpen}
onSelectionApply={onSelectionApply}
hasChanged={hasChanged}
/>
);
}

View file

@ -1,31 +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 { SeriesConfig, SeriesUrl } from '../../types';
import { TRANSACTION_URL } from '../../configurations/constants/elasticsearch_fieldnames';
import { useFilterValues } from '../../series_editor/use_filter_values';
interface Props {
query?: string;
seriesId: number;
series: SeriesUrl;
seriesConfig: SeriesConfig;
}
export const useUrlSearch = ({ series, query, seriesId, seriesConfig }: Props) => {
const { values, loading } = useFilterValues(
{
series,
seriesId,
field: TRANSACTION_URL,
baseFilters: seriesConfig.baseFilters,
label: seriesConfig.labels[TRANSACTION_URL],
},
query
);
return { values, loading };
};

View file

@ -1,66 +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 { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
RECORDS_FIELD,
REPORT_METRIC_FIELD,
REPORT_METRIC_TIMESTAMP,
ReportTypes,
} from '../constants';
export function getAlertsKPIConfig({ spaceId }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.KPI,
defaultSeriesType: 'line',
seriesTypes: [],
xAxisColumn: {
label: i18n.translate('xpack.observability.exploratoryView.alerts.alertStarted', {
defaultMessage: 'Timestamp',
}),
dataType: 'date',
operationType: 'date_histogram',
isBucketed: true,
scale: 'interval',
sourceField: REPORT_METRIC_TIMESTAMP,
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'count',
},
],
hasOperationType: false,
filterFields: ['kibana.alert.rule.name', 'kibana.alert.status'],
breakdownFields: ['kibana.alert.rule.category', 'kibana.alert.status'],
baseFilters: [],
definitionFields: ['kibana.alert.rule.category'],
metricOptions: [
{
label: 'Total alerts',
field: RECORDS_FIELD,
id: 'Alerts',
columnType: 'unique_count',
timestampField: 'kibana.alert.start',
},
{
label: 'Recovered alerts',
field: RECORDS_FIELD,
id: 'recovered_alerts',
columnType: 'unique_count',
timestampField: 'kibana.alert.end',
},
],
labels: { ...FieldLabels },
query: {
language: 'kuery',
query: `kibana.space_ids: "${spaceId}"`,
},
};
}

View file

@ -1,45 +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 { ConfigProps, SeriesConfig } from '../../types';
import { FieldLabels, RECORDS_FIELD, ReportTypes } from '../constants';
export function getAlertsSingleMetricConfig({ spaceId }: ConfigProps): SeriesConfig {
return {
seriesTypes: [],
defaultSeriesType: 'line',
reportType: ReportTypes.SINGLE_METRIC,
xAxisColumn: {},
yAxisColumns: [
{
operationType: 'median',
},
],
hasOperationType: false,
filterFields: ['kibana.alert.rule.name', 'kibana.alert.status'],
breakdownFields: ['kibana.alert.rule.category', 'kibana.alert.status'],
baseFilters: [],
definitionFields: ['kibana.alert.rule.category'],
metricOptions: [
{
label: 'Active',
field: RECORDS_FIELD,
id: 'Alerts',
columnType: 'unique_count',
metricStateOptions: {
titlePosition: 'bottom',
},
emptyAsNull: false,
},
],
labels: { ...FieldLabels },
query: {
language: 'kuery',
query: `kibana.space_ids: "${spaceId}"`,
},
};
}

View file

@ -1,37 +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 { FieldFormat } from '../../types';
import {
METRIC_SYSTEM_CPU_USAGE,
METRIC_SYSTEM_MEMORY_USAGE,
TRANSACTION_DURATION,
} from '../constants/elasticsearch_fieldnames';
export const apmFieldFormats: FieldFormat[] = [
{
field: TRANSACTION_DURATION,
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'asMilliseconds',
outputPrecision: 0,
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: METRIC_SYSTEM_MEMORY_USAGE,
format: { id: 'bytes', params: {} },
},
{
field: METRIC_SYSTEM_CPU_USAGE,
format: { id: 'percent', params: {} },
},
];

View file

@ -1,213 +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 { OperationType } from '@kbn/lens-plugin/public';
import { DOCUMENT_FIELD_NAME } from '@kbn/lens-plugin/common/constants';
import { i18n } from '@kbn/i18n';
import { ReportViewType } from '../../types';
import {
CLS_FIELD,
FCP_FIELD,
FID_FIELD,
LCP_FIELD,
TBT_FIELD,
TRANSACTION_DURATION,
TRANSACTION_TIME_TO_FIRST_BYTE,
} from './elasticsearch_fieldnames';
import {
AGENT_HOST_LABEL,
AGENT_TYPE_LABEL,
BACKEND_TIME_LABEL,
BROWSER_FAMILY_LABEL,
BROWSER_VERSION_LABEL,
CLS_LABEL,
CORE_WEB_VITALS_LABEL,
DCL_LABEL,
DEVICE_DISTRIBUTION_LABEL,
DEVICE_LABEL,
ENVIRONMENT_LABEL,
EVENT_DATASET_LABEL,
FCP_LABEL,
FID_LABEL,
HEATMAP_LABEL,
HOST_NAME_LABEL,
KPI_LABEL,
KPI_OVER_TIME_LABEL,
LABELS_FIELD,
LCP_LABEL,
LOCATION_LABEL,
MESSAGE_LABEL,
METRIC_LABEL,
MONITOR_ID_LABEL,
MONITOR_NAME_LABEL,
MONITOR_STATUS_LABEL,
MONITOR_TYPE_LABEL,
MONITORS_DURATION_LABEL,
OBSERVER_LOCATION_LABEL,
OS_LABEL,
PAGE_LOAD_TIME_LABEL,
PERF_DIST_LABEL,
PORT_LABEL,
REQUEST_METHOD,
SERVICE_NAME_LABEL,
SERVICE_TYPE_LABEL,
SINGLE_METRIC_LABEL,
STEP_DURATION_LABEL,
STEP_NAME_LABEL,
TAGS_LABEL,
TBT_LABEL,
URL_LABEL,
} from './labels';
import {
MONITOR_DURATION_US,
SYNTHETICS_BLOCKED_TIMINGS,
SYNTHETICS_CLS,
SYNTHETICS_CONNECT_TIMINGS,
SYNTHETICS_DCL,
SYNTHETICS_DNS_TIMINGS,
SYNTHETICS_DOCUMENT_ONLOAD,
SYNTHETICS_FCP,
SYNTHETICS_LCP,
SYNTHETICS_RECEIVE_TIMINGS,
SYNTHETICS_SEND_TIMINGS,
SYNTHETICS_SSL_TIMINGS,
SYNTHETICS_STEP_DURATION,
SYNTHETICS_STEP_NAME,
SYNTHETICS_TOTAL_TIMINGS,
SYNTHETICS_WAIT_TIMINGS,
} from './field_names/synthetics';
export const DEFAULT_TIME = { from: 'now-1h', to: 'now' };
export const RECORDS_FIELD = DOCUMENT_FIELD_NAME;
export const RECORDS_PERCENTAGE_FIELD = 'RecordsPercentage';
export const FORMULA_COLUMN = 'FORMULA_COLUMN';
export const FieldLabels: Record<string, string> = {
'user_agent.name': BROWSER_FAMILY_LABEL,
'user_agent.version': BROWSER_VERSION_LABEL,
'user_agent.os.name': OS_LABEL,
'client.geo.country_name': LOCATION_LABEL,
'user_agent.device.name': DEVICE_LABEL,
'observer.geo.name': OBSERVER_LOCATION_LABEL,
'service.name': SERVICE_NAME_LABEL,
'service.environment': ENVIRONMENT_LABEL,
'service.type': SERVICE_TYPE_LABEL,
'event.dataset': EVENT_DATASET_LABEL,
message: MESSAGE_LABEL,
[LCP_FIELD]: LCP_LABEL,
[FCP_FIELD]: FCP_LABEL,
[TBT_FIELD]: TBT_LABEL,
[FID_FIELD]: FID_LABEL,
[CLS_FIELD]: CLS_LABEL,
[SYNTHETICS_CLS]: CLS_LABEL,
[SYNTHETICS_DCL]: DCL_LABEL,
[SYNTHETICS_STEP_DURATION]: STEP_DURATION_LABEL,
[SYNTHETICS_LCP]: LCP_LABEL,
[SYNTHETICS_FCP]: FCP_LABEL,
[SYNTHETICS_DOCUMENT_ONLOAD]: PAGE_LOAD_TIME_LABEL,
[TRANSACTION_TIME_TO_FIRST_BYTE]: BACKEND_TIME_LABEL,
[TRANSACTION_DURATION]: PAGE_LOAD_TIME_LABEL,
[SYNTHETICS_CONNECT_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.connect', {
defaultMessage: 'Connect',
}),
[SYNTHETICS_DNS_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.dns', {
defaultMessage: 'DNS',
}),
[SYNTHETICS_WAIT_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.wait', {
defaultMessage: 'Wait',
}),
[SYNTHETICS_SSL_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.ssl', {
defaultMessage: 'SSL',
}),
[SYNTHETICS_BLOCKED_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.blocked', {
defaultMessage: 'Blocked',
}),
[SYNTHETICS_SEND_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.send', {
defaultMessage: 'Send',
}),
[SYNTHETICS_RECEIVE_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.receive', {
defaultMessage: 'Receive',
}),
[SYNTHETICS_TOTAL_TIMINGS]: i18n.translate('xpack.observability.expView.synthetics.total', {
defaultMessage: 'Total',
}),
'kibana.alert.rule.category': i18n.translate('xpack.observability.expView.alerts.category', {
defaultMessage: 'Rule category',
}),
'kibana.alert.rule.name': i18n.translate('xpack.observability.expView.alerts.name', {
defaultMessage: 'Alert name',
}),
'kibana.alert.status': i18n.translate('xpack.observability.expView.alerts.status', {
defaultMessage: 'Alert status',
}),
'monitor.id': MONITOR_ID_LABEL,
'monitor.status': MONITOR_STATUS_LABEL,
[MONITOR_DURATION_US]: MONITORS_DURATION_LABEL,
[SYNTHETICS_STEP_NAME]: STEP_NAME_LABEL,
'agent.hostname': AGENT_HOST_LABEL,
'agent.type': AGENT_TYPE_LABEL,
'host.hostname': HOST_NAME_LABEL,
'monitor.name': MONITOR_NAME_LABEL,
'monitor.type': MONITOR_TYPE_LABEL,
'url.port': PORT_LABEL,
'url.full': URL_LABEL,
tags: TAGS_LABEL,
// custom
'performance.metric': METRIC_LABEL,
'Business.KPI': KPI_LABEL,
'http.request.method': REQUEST_METHOD,
percentile: 'Percentile',
LABEL_FIELDS_FILTER: LABELS_FIELD,
LABEL_FIELDS_BREAKDOWN: 'Labels field',
};
export const DataViewLabels: Record<ReportViewType, string> = {
'data-distribution': PERF_DIST_LABEL,
'kpi-over-time': KPI_OVER_TIME_LABEL,
'core-web-vitals': CORE_WEB_VITALS_LABEL,
'device-data-distribution': DEVICE_DISTRIBUTION_LABEL,
'single-metric': SINGLE_METRIC_LABEL,
heatmap: HEATMAP_LABEL,
};
export enum ReportTypes {
KPI = 'kpi-over-time',
DISTRIBUTION = 'data-distribution',
CORE_WEB_VITAL = 'core-web-vitals',
DEVICE_DISTRIBUTION = 'device-data-distribution',
SINGLE_METRIC = 'single-metric',
HEATMAP = 'heatmap',
}
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
export const FILTER_RECORDS = 'FILTER_RECORDS';
export const TERMS_COLUMN = 'TERMS_COLUMN';
export const OPERATION_COLUMN = 'operation';
export const PERCENTILE = 'percentile';
export const REPORT_METRIC_FIELD = 'REPORT_METRIC_FIELD';
export const REPORT_METRIC_TIMESTAMP = 'REPORT_METRIC_FIELD_TIMESTAMP';
export const PERCENTILE_RANKS = [
'99th' as OperationType,
'95th' as OperationType,
'90th' as OperationType,
'75th' as OperationType,
'50th' as OperationType,
'25th' as OperationType,
];
export const LABEL_FIELDS_FILTER = 'LABEL_FIELDS_FILTER';
export const LABEL_FIELDS_BREAKDOWN = 'LABEL_FIELDS_BREAKDOWN';
export const ENVIRONMENT_ALL = 'ENVIRONMENT_ALL';

View file

@ -1,10 +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 { RECORDS_FIELD } from '../constants';
export const LOG_RATE = RECORDS_FIELD;

View file

@ -1,11 +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.
*/
export const SYSTEM_CPU_PERCENTAGE_FIELD = 'system.cpu.total.norm.pct';
export const SYSTEM_MEMORY_PERCENTAGE_FIELD = 'system.memory.used.pct';
export const DOCKER_CPU_PERCENTAGE_FIELD = 'docker.cpu.total.pct';
export const K8S_POD_CPU_PERCENTAGE_FIELD = 'kubernetes.pod.cpu.usage.node.pct';

View file

@ -1,35 +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.
*/
export const MONITOR_DURATION_US = 'monitor.duration.us';
export const SYNTHETICS_CLS = 'browser.experience.cls';
export const SYNTHETICS_LCP = 'browser.experience.lcp.us';
export const SYNTHETICS_FCP = 'browser.experience.fcp.us';
export const SYNTHETICS_DOCUMENT_ONLOAD = 'browser.experience.load.us';
export const SYNTHETICS_DCL = 'browser.experience.dcl.us';
export const SYNTHETICS_STEP_NAME = 'synthetics.step.name.keyword';
export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us';
export const SYNTHETICS_DNS_TIMINGS = 'synthetics.payload.timings.dns';
export const SYNTHETICS_SSL_TIMINGS = 'synthetics.payload.timings.ssl';
export const SYNTHETICS_BLOCKED_TIMINGS = 'synthetics.payload.timings.blocked';
export const SYNTHETICS_CONNECT_TIMINGS = 'synthetics.payload.timings.connect';
export const SYNTHETICS_RECEIVE_TIMINGS = 'synthetics.payload.timings.receive';
export const SYNTHETICS_SEND_TIMINGS = 'synthetics.payload.timings.send';
export const SYNTHETICS_WAIT_TIMINGS = 'synthetics.payload.timings.wait';
export const SYNTHETICS_TOTAL_TIMINGS = 'synthetics.payload.timings.total';
export const NETWORK_TIMINGS_FIELDS = [
SYNTHETICS_DNS_TIMINGS,
SYNTHETICS_SSL_TIMINGS,
SYNTHETICS_BLOCKED_TIMINGS,
SYNTHETICS_CONNECT_TIMINGS,
SYNTHETICS_RECEIVE_TIMINGS,
SYNTHETICS_SEND_TIMINGS,
SYNTHETICS_WAIT_TIMINGS,
SYNTHETICS_TOTAL_TIMINGS,
];

View file

@ -1,8 +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.
*/
export * from './constants';

View file

@ -1,408 +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';
export const BROWSER_FAMILY_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.browserFamily',
{
defaultMessage: 'Browser family',
}
);
export const BROWSER_VERSION_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.browserVersion',
{
defaultMessage: 'Browser version',
}
);
export const OS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.os', {
defaultMessage: 'Operating system',
});
export const LOCATION_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.location', {
defaultMessage: 'Location',
});
export const DEVICE_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.device', {
defaultMessage: 'Device',
});
export const OBSERVER_LOCATION_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.obsLocation',
{
defaultMessage: 'Observer location',
}
);
export const SERVICE_NAME_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.serviceName',
{
defaultMessage: 'Service name',
}
);
export const SERVICE_TYPE_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.serviceType',
{
defaultMessage: 'Service type',
}
);
export const ENVIRONMENT_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.environment',
{
defaultMessage: 'Environment',
}
);
export const EVENT_DATASET_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.eventDataset',
{
defaultMessage: 'Dataset',
}
);
export const LCP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.lcp', {
defaultMessage: 'Largest contentful paint',
});
export const FCP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.fcp', {
defaultMessage: 'First contentful paint',
});
export const TBT_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.tbt', {
defaultMessage: 'Total blocking time',
});
export const FID_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.fid', {
defaultMessage: 'First input delay',
});
export const CLS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.cls', {
defaultMessage: 'Cumulative layout shift',
});
export const NETWORK_TIMINGS_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.networkTimings',
{
defaultMessage: 'Network timings',
}
);
export const DCL_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.dcl', {
defaultMessage: 'DOM content loaded',
});
export const DOCUMENT_ONLOAD_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.onload',
{
defaultMessage: 'Document complete (onLoad)',
}
);
export const BACKEND_TIME_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.backend',
{
defaultMessage: 'Backend time',
}
);
export const PAGE_LOAD_TIME_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.pageLoadTime',
{
defaultMessage: 'Page load time',
}
);
export const PAGE_VIEWS_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.pageViews',
{
defaultMessage: 'Page views',
}
);
export const PAGES_LOADED_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.pagesLoaded',
{
defaultMessage: 'Pages loaded',
}
);
export const PINGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.pings', {
defaultMessage: 'Pings',
});
export const MONITOR_ID_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.monitorId',
{
defaultMessage: 'Monitor Id',
}
);
export const MONITOR_STATUS_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.monitorStatus',
{
defaultMessage: 'Monitor Status',
}
);
export const AGENT_HOST_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.agentHost',
{
defaultMessage: 'Agent host',
}
);
export const AGENT_TYPE_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.agentType',
{
defaultMessage: 'Agent type',
}
);
export const MESSAGE_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.message', {
defaultMessage: 'Message',
});
export const HOST_NAME_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.hostName', {
defaultMessage: 'Host name',
});
export const MONITOR_NAME_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.monitorName',
{
defaultMessage: 'Monitor name',
}
);
export const MONITOR_TYPE_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.monitorType',
{
defaultMessage: 'Monitor type',
}
);
export const PORT_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.port', {
defaultMessage: 'Port',
});
export const URL_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.url', {
defaultMessage: 'URL',
});
export const TAGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.tags', {
defaultMessage: 'Tags',
});
export const METRIC_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.metric', {
defaultMessage: 'Metric',
});
export const LABELS_FIELD = i18n.translate('xpack.observability.expView.fieldLabels.labels', {
defaultMessage: 'Labels',
});
export const LABELS_BREAKDOWN = i18n.translate(
'xpack.observability.expView.fieldLabels.chooseField',
{
defaultMessage: 'Labels field',
}
);
export const KPI_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.kpi', {
defaultMessage: 'KPI',
});
export const PERF_DIST_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.performanceDistribution',
{
defaultMessage: 'Performance distribution',
}
);
export const CORE_WEB_VITALS_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.coreWebVitals',
{
defaultMessage: 'Core web vitals',
}
);
export const DEVICE_DISTRIBUTION_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.deviceDistribution',
{
defaultMessage: 'Device distribution',
}
);
export const SINGLE_METRIC_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.singleMetric',
{
defaultMessage: 'Single metric',
}
);
export const HEATMAP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.heatMap', {
defaultMessage: 'Heatmap',
});
export const MOBILE_RESPONSE_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.mobileResponse',
{
defaultMessage: 'Mobile response',
}
);
export const MEMORY_USAGE_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.memoryUsage',
{
defaultMessage: 'System memory usage',
}
);
export const KPI_OVER_TIME_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.kpiOverTime',
{
defaultMessage: 'KPI over time',
}
);
export const MONITORS_DURATION_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.monitorDurationLabel',
{
defaultMessage: 'Monitor duration',
}
);
export const STEP_DURATION_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.stepDurationLabel',
{
defaultMessage: 'Step duration',
}
);
export const STEP_NAME_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.stepNameLabel',
{
defaultMessage: 'Step name',
}
);
export const WEB_APPLICATION_LABEL = i18n.translate(
'xpack.observability.expView.fieldLabels.webApplication',
{
defaultMessage: 'Web Application',
}
);
export const UP_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.upPings', {
defaultMessage: 'Up Pings',
});
export const DOWN_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.downPings', {
defaultMessage: 'Down Pings',
});
export const CARRIER_NAME = i18n.translate('xpack.observability.expView.fieldLabels.carrierName', {
defaultMessage: 'Carrier Name',
});
export const REQUEST_METHOD = i18n.translate(
'xpack.observability.expView.fieldLabels.requestMethod',
{
defaultMessage: 'Request Method',
}
);
export const CONNECTION_TYPE = i18n.translate(
'xpack.observability.expView.fieldLabels.connectionType',
{
defaultMessage: 'Connection Type',
}
);
export const HOST_OS = i18n.translate('xpack.observability.expView.fieldLabels.hostOS', {
defaultMessage: 'Host OS',
});
export const SERVICE_VERSION = i18n.translate(
'xpack.observability.expView.fieldLabels.serviceVersion',
{
defaultMessage: 'Service Version',
}
);
export const OS_PLATFORM = i18n.translate('xpack.observability.expView.fieldLabels.osPlatform', {
defaultMessage: 'OS Platform',
});
export const DEVICE_MODEL = i18n.translate('xpack.observability.expView.fieldLabels.deviceModel', {
defaultMessage: 'Device Model',
});
export const CARRIER_LOCATION = i18n.translate(
'xpack.observability.expView.fieldLabels.carrierLocation',
{
defaultMessage: 'Carrier Location',
}
);
export const RESPONSE_LATENCY = i18n.translate(
'xpack.observability.expView.fieldLabels.responseLatency',
{
defaultMessage: 'Latency',
}
);
export const MOBILE_APP = i18n.translate('xpack.observability.expView.fieldLabels.mobileApp', {
defaultMessage: 'Mobile App',
});
export const SYSTEM_MEMORY_USAGE = i18n.translate(
'xpack.observability.expView.fieldLabels.mobile.memoryUsage',
{
defaultMessage: 'System memory usage',
}
);
export const CPU_USAGE = i18n.translate('xpack.observability.expView.fieldLabels.cpuUsage', {
defaultMessage: 'CPU usage',
});
export const SYSTEM_CPU_USAGE = i18n.translate(
'xpack.observability.expView.fieldLabels.cpuUsage.system',
{
defaultMessage: 'System CPU usage',
}
);
export const DOCKER_CPU_USAGE = i18n.translate(
'xpack.observability.expView.fieldLabels.cpuUsage.docker',
{
defaultMessage: 'Docker CPU usage',
}
);
export const K8S_POD_CPU_USAGE = i18n.translate(
'xpack.observability.expView.fieldLabels.cpuUsage.k8sDocker',
{
defaultMessage: 'K8s pod CPU usage',
}
);
export const TRANSACTIONS_PER_MINUTE = i18n.translate(
'xpack.observability.expView.fieldLabels.transactionPerMinute',
{
defaultMessage: 'Throughput',
}
);
export const NUMBER_OF_DEVICES = i18n.translate(
'xpack.observability.expView.fieldLabels.numberOfDevices',
{
defaultMessage: 'Number of Devices',
}
);
export const LOG_RATE = i18n.translate('xpack.observability.expView.fieldLabels.logRate', {
defaultMessage: 'Log rate',
});

View file

@ -1,22 +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.
*/
export enum URL_KEYS {
DATA_TYPE = 'dt',
OPERATION_TYPE = 'op',
SERIES_TYPE = 'st',
BREAK_DOWN = 'bd',
FILTERS = 'ft',
REPORT_DEFINITIONS = 'rdf',
SELECTED_METRIC = 'mt',
HIDDEN = 'h',
NAME = 'n',
COLOR = 'c',
SHOW_PERCENTILE_ANNOTATIONS = 'spa',
}
export const ALL_VALUES_SELECTED = 'ALL_VALUES';

View file

@ -1,45 +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 { DataView } from '@kbn/data-views-plugin/common';
import { AppDataType, ReportViewType, SeriesConfig } from '../types';
import { ReportConfigMap } from '../contexts/exploratory_view_config';
interface Props {
reportType: ReportViewType;
dataView: DataView;
dataType: AppDataType;
reportConfigMap: ReportConfigMap;
spaceId?: string;
}
export const getDefaultConfigs = ({
reportType,
dataType,
spaceId,
dataView,
reportConfigMap,
}: Props): SeriesConfig => {
let configResult: SeriesConfig | undefined;
reportConfigMap[dataType]?.some((fn) => {
const config = fn({ dataView, spaceId });
if (config.reportType === reportType) {
configResult = config;
}
return config.reportType === reportType;
});
if (!configResult) {
// not a user facing error, more of a dev focused error
throw new Error(
`No report config provided for dataType: ${dataType} and reportType: ${reportType}`
);
}
return configResult;
};

View file

@ -1,62 +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 { createExploratoryViewUrl } from './exploratory_view_url';
import type { AllSeries } from '../../../..';
describe('createExploratoryViewUrl', () => {
const testAllSeries = [
{
dataType: 'synthetics',
seriesType: 'area',
selectedMetricField: 'monitor.duration.us',
time: {
from: 'now-15m',
to: 'now',
},
breakdown: 'monitor.type',
reportDefinitions: {
'monitor.name': [],
'url.full': ['ALL_VALUES'],
},
name: 'All monitors response duration',
},
] as AllSeries;
describe('handles URL reserved chars', () => {
const urlReservedRegex = /[;,\/?:@&=+$#]/;
it('encodes &', () => {
const seriesWithAmpersand = [{ ...testAllSeries[0], name: 'Name with &' }];
const url = createExploratoryViewUrl({
reportType: 'kpi-over-time',
allSeries: seriesWithAmpersand,
});
expect(urlReservedRegex.test(grabRisonQueryFromUrl(url))).toEqual(false);
});
it('encodes other reserved chars', () => {
const seriesWithAmpersand = [
{
...testAllSeries[0],
name: 'Name with URL reserved chars ;,/?:@&=+$#',
},
];
const url = createExploratoryViewUrl({
reportType: 'kpi-over-time',
allSeries: seriesWithAmpersand,
});
expect(urlReservedRegex.test(grabRisonQueryFromUrl(url))).toEqual(false);
});
});
});
function grabRisonQueryFromUrl(url: string) {
return url.split('sr=')[1];
}

View file

@ -1,75 +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 rison from '@kbn/rison';
import { URL_KEYS } from './constants/url_constants';
import type { ReportViewType, SeriesUrl } from '../types';
import type { AllSeries } from '../../../..';
import type { AllShortSeries } from '../hooks/use_series_storage';
export function convertToShortUrl(series: SeriesUrl) {
const {
operationType,
seriesType,
breakdown,
filters,
reportDefinitions,
dataType,
selectedMetricField,
hidden,
name,
color,
...restSeries
} = series;
return {
[URL_KEYS.OPERATION_TYPE]: operationType,
[URL_KEYS.SERIES_TYPE]: seriesType,
[URL_KEYS.BREAK_DOWN]: breakdown,
[URL_KEYS.FILTERS]: filters,
[URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions,
[URL_KEYS.DATA_TYPE]: dataType,
[URL_KEYS.SELECTED_METRIC]: selectedMetricField,
[URL_KEYS.HIDDEN]: hidden,
[URL_KEYS.NAME]: name,
[URL_KEYS.COLOR]: color ? escape(color) : undefined,
...restSeries,
};
}
export function createExploratoryViewUrl(
{ reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries },
baseHref = '',
appId = 'observability'
) {
const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series));
return (
baseHref +
`/app/${appId}/exploratory-view/#?reportType=${reportType}&sr=${encodeUriIfNeeded(
rison.encode(allShortSeries)
)}`
);
}
/**
* Encodes the uri if it contains characters (`/?@&=+#`).
* It doesn't consider `,` and `:` as they are part of [Rison]{@link https://www.npmjs.com/package/rison-node} syntax.
*
* @param uri Non encoded URI
*/
export function encodeUriIfNeeded(uri: string) {
if (!uri) {
return uri;
}
if (/[\/?@&=+#]/.test(uri)) {
return encodeURIComponent(uri);
}
return uri;
}

View file

@ -1,58 +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 { ConfigProps, SeriesConfig } from '../../types';
import { FieldLabels, RECORDS_FIELD, ReportTypes } from '../constants';
import { LOG_RATE as LOG_RATE_FIELD } from '../constants/field_names/infra_logs';
import { LOG_RATE as LOG_RATE_LABEL } from '../constants/labels';
export function getLogsKPIConfig(configProps: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.KPI,
defaultSeriesType: 'bar',
seriesTypes: [],
xAxisColumn: {
label: i18n.translate('xpack.observability.exploratoryView.logs.logRateXAxisLabel', {
defaultMessage: 'Timestamp',
}),
dataType: 'date',
operationType: 'date_histogram',
sourceField: '@timestamp',
isBucketed: true,
scale: 'interval',
},
yAxisColumns: [
{
label: i18n.translate('xpack.observability.exploratoryView.logs.logRateYAxisLabel', {
defaultMessage: 'Log rate per minute',
}),
dataType: 'number',
operationType: 'count',
isBucketed: false,
scale: 'ratio',
sourceField: RECORDS_FIELD,
timeScale: 'm',
},
],
hasOperationType: false,
filterFields: ['agent.type', 'service.type', 'event.dataset'],
breakdownFields: ['agent.hostname', 'service.type', 'event.dataset'],
baseFilters: [],
definitionFields: ['agent.hostname', 'service.type', 'event.dataset'],
textDefinitionFields: ['message'],
metricOptions: [
{
label: LOG_RATE_LABEL,
field: RECORDS_FIELD,
id: LOG_RATE_FIELD,
columnType: 'unique_count',
},
],
labels: { ...FieldLabels },
};
}

View file

@ -1,45 +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 { FieldFormat } from '../../types';
import {
SYSTEM_CPU_PERCENTAGE_FIELD,
DOCKER_CPU_PERCENTAGE_FIELD,
K8S_POD_CPU_PERCENTAGE_FIELD,
SYSTEM_MEMORY_PERCENTAGE_FIELD,
} from '../constants/field_names/infra_metrics';
export const infraMetricsFieldFormats: FieldFormat[] = [
{
field: SYSTEM_CPU_PERCENTAGE_FIELD,
format: {
id: 'percent',
params: {},
},
},
{
field: DOCKER_CPU_PERCENTAGE_FIELD,
format: {
id: 'percent',
params: {},
},
},
{
field: K8S_POD_CPU_PERCENTAGE_FIELD,
format: {
id: 'percent',
params: {},
},
},
{
field: SYSTEM_MEMORY_PERCENTAGE_FIELD,
format: {
id: 'percent',
params: {},
},
},
];

View file

@ -1,70 +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 { ConfigProps, SeriesConfig } from '../../types';
import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
import {
SYSTEM_CPU_PERCENTAGE_FIELD,
DOCKER_CPU_PERCENTAGE_FIELD,
K8S_POD_CPU_PERCENTAGE_FIELD,
SYSTEM_MEMORY_PERCENTAGE_FIELD,
} from '../constants/field_names/infra_metrics';
import {
DOCKER_CPU_USAGE,
K8S_POD_CPU_USAGE,
SYSTEM_CPU_USAGE,
SYSTEM_MEMORY_USAGE,
} from '../constants/labels';
export function getMetricsKPIConfig({ dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.KPI,
defaultSeriesType: 'area',
seriesTypes: [],
xAxisColumn: {
sourceField: '@timestamp',
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'median',
},
],
hasOperationType: false,
filterFields: ['agent.type', 'service.type'],
breakdownFields: ['agent.hostname', 'service.type'],
baseFilters: [],
definitionFields: ['agent.hostname', 'service.type'],
metricOptions: [
{
label: SYSTEM_CPU_USAGE,
field: SYSTEM_CPU_PERCENTAGE_FIELD,
id: SYSTEM_CPU_PERCENTAGE_FIELD,
columnType: OPERATION_COLUMN,
},
{
label: SYSTEM_MEMORY_USAGE,
field: SYSTEM_MEMORY_PERCENTAGE_FIELD,
id: SYSTEM_MEMORY_PERCENTAGE_FIELD,
columnType: OPERATION_COLUMN,
},
{
label: DOCKER_CPU_USAGE,
field: DOCKER_CPU_PERCENTAGE_FIELD,
id: DOCKER_CPU_PERCENTAGE_FIELD,
columnType: OPERATION_COLUMN,
},
{
label: K8S_POD_CPU_USAGE,
field: K8S_POD_CPU_PERCENTAGE_FIELD,
id: K8S_POD_CPU_PERCENTAGE_FIELD,
columnType: OPERATION_COLUMN,
},
],
labels: { ...FieldLabels },
};
}

View file

@ -1,685 +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 { LayerConfig, LensAttributes } from './lens_attributes';
import { mockAppDataView, mockDataView } from '../rtl_helpers';
import { getDefaultConfigs } from './default_configs';
import { sampleAttribute } from './test_data/sample_attribute';
import {
LCP_FIELD,
TRANSACTION_DURATION,
USER_AGENT_NAME,
} from './constants/elasticsearch_fieldnames';
import { buildExistsFilter, buildPhrasesFilter } from './utils';
import { sampleAttributeKpi } from './test_data/sample_attribute_kpi';
import { RECORDS_FIELD, REPORT_METRIC_FIELD, PERCENTILE_RANKS, ReportTypes } from './constants';
import { obsvReportConfigMap } from '../obsv_exploratory_view';
import { sampleAttributeWithReferenceLines } from './test_data/sample_attribute_with_reference_lines';
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
import { FormulaPublicApi, XYState } from '@kbn/lens-plugin/public';
describe('Lens Attribute', () => {
mockAppDataView();
const reportViewConfig = getDefaultConfigs({
reportType: 'data-distribution',
dataType: 'ux',
dataView: mockDataView,
reportConfigMap: obsvReportConfigMap,
});
reportViewConfig.baseFilters?.push(...buildExistsFilter('transaction.type', mockDataView));
let lnsAttr: LensAttributes;
const layerConfig: LayerConfig = {
seriesConfig: reportViewConfig,
seriesType: 'line',
operationType: 'count',
dataView: mockDataView,
reportDefinitions: {},
time: { from: 'now-15m', to: 'now' },
color: 'green',
name: 'test-series',
selectedMetricField: TRANSACTION_DURATION,
};
const lensPluginMockStart = lensPluginMock.createStartContract();
let formulaHelper: FormulaPublicApi;
beforeEach(async () => {
formulaHelper = (await lensPluginMockStart.stateHelperApi()).formula;
lnsAttr = new LensAttributes([layerConfig], reportViewConfig.reportType, formulaHelper);
});
it('should return expected json', function () {
expect(lnsAttr.getJSON()).toEqual(sampleAttribute);
});
it('should return expected json for kpi report type', function () {
const seriesConfigKpi = getDefaultConfigs({
reportType: ReportTypes.KPI,
dataType: 'ux',
dataView: mockDataView,
reportConfigMap: obsvReportConfigMap,
});
const lnsAttrKpi = new LensAttributes(
[
{
seriesConfig: seriesConfigKpi,
seriesType: 'line',
operationType: 'count',
dataView: mockDataView,
reportDefinitions: { 'service.name': ['elastic-co'] },
time: { from: 'now-15m', to: 'now' },
color: 'green',
name: 'test-series',
selectedMetricField: RECORDS_FIELD,
},
],
ReportTypes.KPI
);
expect(lnsAttrKpi.getJSON()).toEqual(sampleAttributeKpi);
});
it('should return expected json for percentile breakdowns', function () {
const seriesConfigKpi = getDefaultConfigs({
reportType: ReportTypes.KPI,
dataType: 'ux',
dataView: mockDataView,
reportConfigMap: obsvReportConfigMap,
});
const lnsAttrKpi = new LensAttributes(
[
{
filters: [],
seriesConfig: seriesConfigKpi,
time: {
from: 'now-1h',
to: 'now',
},
dataView: mockDataView,
name: 'Page load time',
breakdown: 'percentile',
reportDefinitions: {},
selectedMetricField: 'transaction.duration.us',
color: '#54b399',
},
],
ReportTypes.KPI
);
expect(lnsAttrKpi.getJSON().state.datasourceStates.formBased.layers.layer0.columns).toEqual({
'x-axis-column-layer0': {
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: {
interval: 'auto',
includeEmptyRows: true,
},
scale: 'interval',
sourceField: '@timestamp',
},
...PERCENTILE_RANKS.reduce((acc: Record<string, any>, rank, index) => {
acc[`y-axis-column-${index === 0 ? 'layer' + index + '-0' : index}`] = {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query: 'transaction.type: page-load and processor.event: transaction',
},
isBucketed: false,
label: 'Page load time',
operationType: 'percentile',
params: {
percentile: Number(rank.slice(0, 2)),
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
};
return acc;
}, {}),
});
});
it('should return main y axis', function () {
expect(lnsAttr.getMainYAxis(layerConfig, 'layer0', '')).toEqual([
{
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'test-series',
operationType: 'formula',
params: {
format: {
id: 'percent',
params: {
decimals: 0,
},
},
formula: 'count() / overall_sum(count())',
isFormulaBroken: false,
},
references: ['y-axis-column-layer0X3'],
},
]);
});
it('should return expected field type', function () {
expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
name: 'transaction.type',
type: 'string',
esTypes: ['keyword'],
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
fieldName: 'transaction.type',
})
);
});
it('should return expected field type for custom field with default value', function () {
expect(JSON.stringify(lnsAttr.getFieldMeta(REPORT_METRIC_FIELD, layerConfig))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
name: 'transaction.duration.us',
type: 'number',
esTypes: ['long'],
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
fieldName: 'transaction.duration.us',
columnLabel: 'Page load time',
showPercentileAnnotations: true,
})
);
});
it('should return expected field type for custom field with passed value', function () {
const layerConfig1: LayerConfig = {
seriesConfig: reportViewConfig,
seriesType: 'line',
operationType: 'count',
dataView: mockDataView,
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
time: { from: 'now-15m', to: 'now' },
color: 'green',
name: 'test-series',
selectedMetricField: TRANSACTION_DURATION,
};
lnsAttr = new LensAttributes([layerConfig1], reportViewConfig.reportType, formulaHelper);
expect(JSON.stringify(lnsAttr.getFieldMeta(REPORT_METRIC_FIELD, layerConfig1))).toEqual(
JSON.stringify({
fieldMeta: {
count: 0,
name: TRANSACTION_DURATION,
type: 'number',
esTypes: ['long'],
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
fieldName: TRANSACTION_DURATION,
columnLabel: 'Page load time',
showPercentileAnnotations: true,
})
);
});
it('should return expected number range column', function () {
expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
operationType: 'range',
params: {
maxBars: 'auto',
ranges: [
{
from: 0,
label: '',
to: 1000,
},
],
type: 'histogram',
},
scale: 'interval',
sourceField: 'transaction.duration.us',
});
});
it('should return expected number operation column', function () {
expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
operationType: 'range',
params: {
maxBars: 'auto',
ranges: [
{
from: 0,
label: '',
to: 1000,
},
],
type: 'histogram',
},
scale: 'interval',
sourceField: 'transaction.duration.us',
});
});
it('should return expected date histogram column', function () {
expect(lnsAttr.getDateHistogramColumn('@timestamp')).toEqual({
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: {
interval: 'auto',
includeEmptyRows: true,
},
scale: 'interval',
sourceField: '@timestamp',
});
});
it('should return main x axis', function () {
expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({
dataType: 'number',
isBucketed: true,
label: 'Page load time',
operationType: 'range',
params: {
maxBars: 'auto',
ranges: [
{
from: 0,
label: '',
to: 1000,
},
],
type: 'histogram',
},
scale: 'interval',
sourceField: 'transaction.duration.us',
});
});
it('should hide y axis when there are multiple series', function () {
const lensAttrWithMultiSeries = new LensAttributes(
[layerConfig, layerConfig],
reportViewConfig.reportType,
formulaHelper
).getJSON() as any;
expect(lensAttrWithMultiSeries.state.visualization.axisTitlesVisibilitySettings).toEqual({
x: false,
yLeft: false,
yRight: false,
});
});
it('should show y axis when there is a single series', function () {
const lensAttrWithMultiSeries = new LensAttributes(
[layerConfig],
reportViewConfig.reportType,
formulaHelper
).getJSON() as any;
expect(lensAttrWithMultiSeries.state.visualization.axisTitlesVisibilitySettings).toEqual({
x: false,
yLeft: true,
yRight: true,
});
});
it('should return first layer', function () {
expect(lnsAttr.getLayers()).toEqual(sampleAttribute.state.datasourceStates.formBased.layers);
});
it('should return expected XYState', function () {
expect(lnsAttr.getXyState()).toEqual({
axisTitlesVisibilitySettings: { x: false, yLeft: true, yRight: true },
curveType: 'CURVE_MONOTONE_X',
fittingFunction: 'Linear',
gridlinesVisibilitySettings: { x: false, yLeft: true, yRight: true },
layers: [
{
accessors: ['y-axis-column-layer0-0'],
layerId: 'layer0',
layerType: 'data',
palette: undefined,
seriesType: 'line',
xAccessor: 'x-axis-column-layer0',
yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0-0', axisMode: 'left' }],
},
{
accessors: [
'50th-percentile-reference-line-layer0-reference-lines',
'75th-percentile-reference-line-layer0-reference-lines',
'90th-percentile-reference-line-layer0-reference-lines',
'95th-percentile-reference-line-layer0-reference-lines',
'99th-percentile-reference-line-layer0-reference-lines',
],
layerId: 'layer0-reference-lines',
layerType: 'referenceLine',
yConfig: [
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '50th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '75th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '90th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '95th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '99th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
],
},
],
legend: {
isVisible: true,
showSingleSeries: true,
position: 'right',
legendSize: 'auto',
shouldTruncate: false,
},
preferredSeriesType: 'line',
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
valueLabels: 'hide',
});
});
it('should not use global filters when there is more than one series', function () {
const multiSeriesLensAttr = new LensAttributes(
[layerConfig, layerConfig],
reportViewConfig.reportType,
formulaHelper
).getJSON();
expect(multiSeriesLensAttr.state.query.query).toEqual('transaction.duration.us < 60000000');
});
describe('Layer breakdowns', function () {
it('should return breakdown column', function () {
const layerConfig1: LayerConfig = {
seriesConfig: reportViewConfig,
seriesType: 'line',
operationType: 'count',
dataView: mockDataView,
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
breakdown: USER_AGENT_NAME,
time: { from: 'now-15m', to: 'now' },
color: 'green',
name: 'test-series',
selectedMetricField: LCP_FIELD,
};
lnsAttr = new LensAttributes([layerConfig1], reportViewConfig.reportType, formulaHelper);
lnsAttr.getBreakdownColumn({
layerConfig: layerConfig1,
sourceField: USER_AGENT_NAME,
layerId: 'layer0',
});
expect((lnsAttr.visualization as XYState)?.layers).toEqual([
{
accessors: ['y-axis-column-layer0-0'],
layerId: 'layer0',
layerType: 'data',
palette: undefined,
seriesType: 'line',
splitAccessor: 'breakdown-column-layer0',
xAccessor: 'x-axis-column-layer0',
yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0-0', axisMode: 'left' }],
},
]);
expect(lnsAttr.layers.layer0).toEqual({
columnOrder: [
'breakdown-column-layer0',
'x-axis-column-layer0',
'y-axis-column-layer0-0',
'y-axis-column-layer0X0',
'y-axis-column-layer0X1',
'y-axis-column-layer0X2',
'y-axis-column-layer0X3',
],
columns: {
'breakdown-column-layer0': {
dataType: 'string',
isBucketed: true,
label: 'Browser family',
operationType: 'terms',
params: {
missingBucket: false,
orderAgg: {
dataType: 'number',
isBucketed: false,
label: 'Count of records',
operationType: 'count',
scale: 'ratio',
sourceField: '___records___',
},
orderBy: {
type: 'custom',
},
orderDirection: 'desc',
otherBucket: true,
size: 10,
},
scale: 'ordinal',
sourceField: 'user_agent.name',
},
'x-axis-column-layer0': {
dataType: 'number',
isBucketed: true,
label: 'Largest contentful paint',
operationType: 'range',
params: {
maxBars: 'auto',
ranges: [
{
from: 0,
label: '',
to: 1000,
},
],
type: 'histogram',
},
scale: 'interval',
sourceField: LCP_FIELD,
},
'y-axis-column-layer0-0': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
isBucketed: false,
label: 'test-series',
operationType: 'formula',
params: {
format: {
id: 'percent',
params: {
decimals: 0,
},
},
formula:
"count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *') / overall_sum(count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *'))",
isFormulaBroken: false,
},
references: ['y-axis-column-layer0X3'],
},
'y-axis-column-layer0X0': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: RECORDS_FIELD,
},
'y-axis-column-layer0X1': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: RECORDS_FIELD,
},
'y-axis-column-layer0X2': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'overall_sum',
references: ['y-axis-column-layer0X1'],
scale: 'ratio',
},
'y-axis-column-layer0X3': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'math',
params: {
tinymathAst: {
args: ['y-axis-column-layer0X0', 'y-axis-column-layer0X2'],
location: {
max: 212,
min: 0,
},
name: 'divide',
text: "count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *') / overall_sum(count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *'))",
type: 'function',
},
},
references: ['y-axis-column-layer0X0', 'y-axis-column-layer0X2'],
scale: 'ratio',
},
},
incompleteColumns: {},
});
});
});
describe('Layer Filters', function () {
it('should return expected filters', function () {
reportViewConfig.baseFilters?.push(
...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockDataView)
);
const layerConfig1: LayerConfig = {
seriesConfig: reportViewConfig,
seriesType: 'line',
operationType: 'count',
dataView: mockDataView,
reportDefinitions: { 'performance.metric': [LCP_FIELD] },
time: { from: 'now-15m', to: 'now' },
color: 'green',
name: 'test-series',
selectedMetricField: TRANSACTION_DURATION,
};
const filters = lnsAttr.getLayerFilters(layerConfig1, 2);
expect(filters).toEqual(
'@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)'
);
});
});
describe('Reference line layers', function () {
it('should return expected reference lines', function () {
const layerConfig1: LayerConfig = {
seriesConfig: reportViewConfig,
seriesType: 'line',
dataView: mockDataView,
reportDefinitions: {},
time: { from: 'now-15m', to: 'now' },
color: 'green',
name: 'test-series',
selectedMetricField: TRANSACTION_DURATION,
};
lnsAttr = new LensAttributes([layerConfig1], reportViewConfig.reportType, formulaHelper);
const attributes = lnsAttr.getJSON();
expect(attributes).toEqual(sampleAttributeWithReferenceLines);
});
});
});

View file

@ -1,96 +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 { FormulaPublicApi, HeatmapVisualizationState } from '@kbn/lens-plugin/public';
import { euiPaletteNegative } from '@elastic/eui';
import { ColorStop } from '@kbn/coloring';
import { LayerConfig } from '../lens_attributes';
import { SingleMetricLensAttributes } from './single_metric_attributes';
export class HeatMapLensAttributes extends SingleMetricLensAttributes {
xColumnId: string;
layerId: string;
breakDownColumnId: string;
constructor(
layerConfigs: LayerConfig[],
reportType: string,
lensFormulaHelper: FormulaPublicApi
) {
super(layerConfigs, reportType, lensFormulaHelper);
this.xColumnId = 'layer-0-column-x-1';
this.breakDownColumnId = 'layer-0-breakdown-column';
this.layerId = 'layer0';
const layer0 = this.getSingleMetricLayer()!;
layer0.columns[this.xColumnId] = this.getDateHistogramColumn('@timestamp');
let columnOrder = [this.xColumnId];
const layerConfig = layerConfigs[0];
if (layerConfig.breakdown) {
columnOrder = [this.breakDownColumnId, ...columnOrder];
layer0.columns[this.breakDownColumnId] = this.getBreakdownColumn({
layerConfig,
sourceField: layerConfig.breakdown,
layerId: this.layerId,
alphabeticOrder: true,
});
}
layer0.columnOrder = [...columnOrder, ...layer0.columnOrder];
this.layers = { layer0 };
this.visualization = this.getHeatmapState();
}
getHeatmapState() {
const negativePalette = euiPaletteNegative(5);
const layerConfig = this.layerConfigs[0];
return {
shape: 'heatmap',
layerId: this.layerId,
layerType: 'data',
legend: {
isVisible: true,
position: 'right',
type: 'heatmap_legend',
},
gridConfig: {
type: 'heatmap_grid',
isCellLabelVisible: false,
isYAxisLabelVisible: true,
isXAxisLabelVisible: true,
isYAxisTitleVisible: false,
isXAxisTitleVisible: false,
xTitle: '',
},
valueAccessor: this.columnId,
xAccessor: this.xColumnId,
yAccessor: layerConfig.breakdown ? this.breakDownColumnId : undefined,
palette: {
type: 'palette',
name: 'negative',
params: {
name: 'negative',
continuity: 'above',
reverse: false,
stops: negativePalette.map((nColor, ind) => ({
color: nColor,
stop: ind === 0 ? 1 : ind * 20,
})) as ColorStop[],
rangeMin: 0,
},
accessor: this.columnId,
},
} as HeatmapVisualizationState;
}
}

View file

@ -1,213 +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 { SingleMetricLensAttributes } from './single_metric_attributes';
import { ReportTypes } from '../../../../..';
import { mockAppDataView, mockDataView } from '../../rtl_helpers';
import { getDefaultConfigs } from '../default_configs';
import { obsvReportConfigMap } from '../../obsv_exploratory_view';
import { buildExistsFilter } from '../utils';
import { LensAttributes } from '../lens_attributes';
import { TRANSACTION_DURATION } from '../constants/elasticsearch_fieldnames';
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
import { FormulaPublicApi } from '@kbn/lens-plugin/public';
import { sampleMetricFormulaAttribute } from '../test_data/test_formula_metric_attribute';
import { DataTypes } from '../..';
describe('SingleMetricAttributes', () => {
mockAppDataView();
const reportViewConfig = getDefaultConfigs({
reportType: ReportTypes.SINGLE_METRIC,
dataType: 'ux',
dataView: mockDataView,
reportConfigMap: obsvReportConfigMap,
});
reportViewConfig.baseFilters?.push(...buildExistsFilter('transaction.type', mockDataView));
let lnsAttr: LensAttributes;
const layerConfig: any = {
seriesConfig: reportViewConfig,
operationType: 'median',
dataView: mockDataView,
reportDefinitions: {},
time: { from: 'now-15m', to: 'now' },
name: 'Page load time',
selectedMetricField: TRANSACTION_DURATION,
};
const lensPluginMockStart = lensPluginMock.createStartContract();
let formulaHelper: FormulaPublicApi;
beforeEach(async () => {
formulaHelper = (await lensPluginMockStart.stateHelperApi()).formula;
lnsAttr = new SingleMetricLensAttributes(
[layerConfig],
ReportTypes.SINGLE_METRIC,
formulaHelper
);
});
it('returns attributes as expected', () => {
const jsonAttr = lnsAttr.getJSON('lnsLegacyMetric');
expect(jsonAttr).toEqual({
description: '',
references: [],
state: {
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
],
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: ['layer-0-column-1'],
columns: {
'layer-0-column-1': {
dataType: 'number',
isBucketed: false,
label: 'Page load time',
operationType: 'median',
scale: 'ratio',
sourceField: 'transaction.duration.us',
params: {
emptyAsNull: true,
},
},
},
incompleteColumns: {},
},
},
},
},
filters: [],
query: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
visualization: {
accessor: 'layer-0-column-1',
layerId: 'layer0',
layerType: 'data',
size: 's',
},
},
title: 'Prefilled from exploratory view app',
visualizationType: 'lnsLegacyMetric',
});
});
it('returns attributes as expected for percentile operation', () => {
layerConfig.operationType = '99th';
lnsAttr = new SingleMetricLensAttributes(
[layerConfig],
ReportTypes.SINGLE_METRIC,
formulaHelper
);
const jsonAttr = lnsAttr.getJSON('lnsLegacyMetric');
expect(jsonAttr).toEqual({
description: '',
references: [],
state: {
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
],
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: ['layer-0-column-1'],
columns: {
'layer-0-column-1': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Page load time',
operationType: 'percentile',
params: {
percentile: 99,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
},
incompleteColumns: {},
},
},
},
},
filters: [],
query: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
visualization: {
accessor: 'layer-0-column-1',
layerId: 'layer0',
layerType: 'data',
size: 's',
},
},
title: 'Prefilled from exploratory view app',
visualizationType: 'lnsLegacyMetric',
});
});
it('returns attributes as expected for formula column', () => {
const reportViewConfigFormula = getDefaultConfigs({
reportType: ReportTypes.SINGLE_METRIC,
dataType: DataTypes.SYNTHETICS,
dataView: mockDataView,
reportConfigMap: obsvReportConfigMap,
});
const layerConfigFormula: any = {
seriesConfig: reportViewConfigFormula,
operationType: 'median',
dataView: mockDataView,
reportDefinitions: {},
time: { from: 'now-15m', to: 'now' },
name: 'Availability',
selectedMetricField: 'monitor_availability',
};
lnsAttr = new SingleMetricLensAttributes(
[layerConfigFormula],
ReportTypes.SINGLE_METRIC,
formulaHelper
);
const jsonAttr = lnsAttr.getJSON('lnsLegacyMetric');
expect(jsonAttr).toEqual(sampleMetricFormulaAttribute);
});
});

View file

@ -1,201 +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 { FormulaPublicApi, MetricState, OperationType } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/common';
import { Query } from '@kbn/es-query';
import { getColorPalette } from '../synthetics/single_metric_config';
import { FORMULA_COLUMN, RECORDS_FIELD } from '../constants';
import { ColumnFilter, MetricOption } from '../../types';
import { SeriesConfig } from '../../../../..';
import {
buildNumberColumn,
LayerConfig,
LensAttributes,
parseCustomFieldName,
} from '../lens_attributes';
export class SingleMetricLensAttributes extends LensAttributes {
columnId: string;
metricStateOptions?: MetricOption['metricStateOptions'];
constructor(
layerConfigs: LayerConfig[],
reportType: string,
lensFormulaHelper: FormulaPublicApi
) {
super(layerConfigs, reportType, lensFormulaHelper);
this.layers = {};
this.reportType = reportType;
this.layerConfigs = layerConfigs;
this.isMultiSeries = layerConfigs.length > 1;
this.columnId = 'layer-0-column-1';
this.globalFilter = this.getGlobalFilter(this.isMultiSeries);
const layer0 = this.getSingleMetricLayer()!;
this.layers = {
layer0,
};
this.visualization = this.getMetricState();
}
getSingleMetricLayer() {
const { seriesConfig, selectedMetricField, operationType, dataView, name } =
this.layerConfigs[0];
const metricOption = parseCustomFieldName(seriesConfig, selectedMetricField);
if (!Array.isArray(metricOption)) {
const {
columnFilter,
columnField,
columnLabel,
columnType,
formula,
metricStateOptions,
format,
emptyAsNull = true,
} = metricOption;
this.metricStateOptions = metricStateOptions;
if (columnType === FORMULA_COLUMN && formula) {
return this.getFormulaLayer({
formula,
label: name ?? columnLabel,
dataView,
format,
filter: columnFilter,
});
}
const getSourceField = () => {
if (
selectedMetricField.startsWith('Records') ||
selectedMetricField.startsWith('records')
) {
return 'Records';
}
return columnField || selectedMetricField;
};
const sourceField = getSourceField();
const isPercentileColumn = operationType?.includes('th');
if (isPercentileColumn) {
return this.getPercentileLayer({
sourceField,
operationType,
seriesConfig,
columnLabel,
columnFilter,
});
}
return {
columns: {
[this.columnId]: {
...buildNumberColumn(sourceField),
label: name ?? columnLabel,
operationType: sourceField === RECORDS_FIELD ? 'count' : operationType || 'median',
filter: columnFilter,
params: {
emptyAsNull,
},
},
},
columnOrder: [this.columnId],
incompleteColumns: {},
};
}
}
getFormulaLayer({
formula,
label,
dataView,
format,
filter,
}: {
formula: string;
label?: string;
format?: string;
filter?: Query;
dataView: DataView;
}) {
const layer = this.lensFormulaHelper?.insertOrReplaceFormulaColumn(
this.columnId,
{
formula,
label,
filter,
format:
format === 'percent' || !format
? {
id: 'percent',
params: {
decimals: 1,
},
}
: undefined,
},
{ columns: {}, columnOrder: [] },
dataView
);
return layer!;
}
getPercentileLayer({
sourceField,
operationType,
seriesConfig,
columnLabel,
columnFilter,
}: {
sourceField: string;
operationType?: OperationType;
seriesConfig: SeriesConfig;
columnLabel?: string;
columnFilter?: ColumnFilter;
}) {
return {
columns: {
[this.columnId]: {
...this.getPercentileNumberColumn(sourceField, operationType!, seriesConfig),
label: columnLabel ?? '',
filter: columnFilter,
},
},
columnOrder: [this.columnId],
incompleteColumns: {},
};
}
getMetricState(): MetricState {
const { color } = this.layerConfigs[0];
const metricStateOptions: MetricOption['metricStateOptions'] = {
...(this.metricStateOptions ?? {}),
...(color ? { colorMode: 'Labels', palette: getColorPalette(color) } : {}),
};
return {
accessor: this.columnId,
layerId: 'layer0',
layerType: 'data',
...metricStateOptions,
size: 's',
};
}
}

View file

@ -1,61 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FormulaIndexPatternColumn, FormulaPublicApi } from '@kbn/lens-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/common';
export function getDistributionInPercentageColumn({
label,
layerId,
dataView,
columnFilter,
lensFormulaHelper,
formula,
format,
}: {
label?: string;
columnFilter?: string;
layerId: string;
lensFormulaHelper: FormulaPublicApi;
dataView: DataView;
formula?: string;
format?: string;
}) {
const yAxisColId = `y-axis-column-${layerId}`;
let lensFormula = formula ?? 'count() / overall_sum(count())';
if (columnFilter) {
lensFormula =
formula ?? `count(kql='${columnFilter}') / overall_sum(count(kql='${columnFilter}'))`;
}
const { columns } = lensFormulaHelper?.insertOrReplaceFormulaColumn(
yAxisColId,
{
formula: lensFormula,
label,
format:
format === 'percent' || !format
? {
id: 'percent',
params: {
decimals: 0,
},
}
: undefined,
},
{
columns: {},
columnOrder: [],
},
dataView
) ?? { columns: {} };
const { [yAxisColId]: main, ...supportingColumns } = columns;
return { main: columns[yAxisColId] as FormulaIndexPatternColumn, supportingColumns };
}

View file

@ -1,56 +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 { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
LABEL_FIELDS_FILTER,
REPORT_METRIC_FIELD,
ReportTypes,
USE_BREAK_DOWN_COLUMN,
} from '../constants';
import { buildPhraseFilter } from '../utils';
import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames';
import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels';
import { MobileFields } from './mobile_fields';
export function getMobileDeviceDistributionConfig({ dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.DEVICE_DISTRIBUTION,
defaultSeriesType: 'bar',
seriesTypes: ['bar', 'bar_horizontal'],
xAxisColumn: {
sourceField: USE_BREAK_DOWN_COLUMN,
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'unique_count',
},
],
hasOperationType: false,
filterFields: [...Object.keys(MobileFields), LABEL_FIELDS_FILTER],
breakdownFields: Object.keys(MobileFields),
baseFilters: [
...buildPhraseFilter('agent.name', 'iOS/swift', dataView),
...buildPhraseFilter('processor.event', 'transaction', dataView),
],
labels: {
...FieldLabels,
...MobileFields,
[SERVICE_NAME]: MOBILE_APP,
},
definitionFields: [SERVICE_NAME],
metricOptions: [
{
field: 'labels.device_id',
id: 'labels.device_id',
label: NUMBER_OF_DEVICES,
},
],
};
}

View file

@ -1,90 +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 { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
LABEL_FIELDS_FILTER,
RECORDS_FIELD,
REPORT_METRIC_FIELD,
ReportTypes,
} from '../constants';
import { buildPhrasesFilter } from '../utils';
import {
METRIC_SYSTEM_CPU_USAGE,
METRIC_SYSTEM_MEMORY_USAGE,
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_DURATION,
} from '../constants/elasticsearch_fieldnames';
import { CPU_USAGE, SYSTEM_MEMORY_USAGE, MOBILE_APP, RESPONSE_LATENCY } from '../constants/labels';
import { MobileFields } from './mobile_fields';
export function getMobileKPIDistributionConfig({ dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: 'bar',
seriesTypes: ['line', 'bar'],
xAxisColumn: {
sourceField: REPORT_METRIC_FIELD,
},
yAxisColumns: [
{
sourceField: RECORDS_FIELD,
},
],
hasOperationType: false,
filterFields: [...Object.keys(MobileFields), LABEL_FIELDS_FILTER],
breakdownFields: Object.keys(MobileFields),
baseFilters: [
...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], dataView),
],
labels: {
...FieldLabels,
...MobileFields,
[SERVICE_NAME]: MOBILE_APP,
},
definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT],
metricOptions: [
{
label: RESPONSE_LATENCY,
field: TRANSACTION_DURATION,
id: TRANSACTION_DURATION,
columnFilters: [
{
language: 'kuery',
query: `${PROCESSOR_EVENT}: transaction`,
},
],
},
{
label: SYSTEM_MEMORY_USAGE,
field: METRIC_SYSTEM_MEMORY_USAGE,
id: METRIC_SYSTEM_MEMORY_USAGE,
columnFilters: [
{
language: 'kuery',
query: `${PROCESSOR_EVENT}: metric`,
},
],
},
{
label: CPU_USAGE,
field: METRIC_SYSTEM_CPU_USAGE,
id: METRIC_SYSTEM_CPU_USAGE,
columnFilters: [
{
language: 'kuery',
query: `${PROCESSOR_EVENT}: metric`,
},
],
},
],
};
}

View file

@ -1,109 +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 { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
LABEL_FIELDS_FILTER,
OPERATION_COLUMN,
RECORDS_FIELD,
REPORT_METRIC_FIELD,
ReportTypes,
} from '../constants';
import { buildPhrasesFilter } from '../utils';
import {
METRIC_SYSTEM_CPU_USAGE,
METRIC_SYSTEM_MEMORY_USAGE,
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_DURATION,
} from '../constants/elasticsearch_fieldnames';
import {
CPU_USAGE,
SYSTEM_MEMORY_USAGE,
MOBILE_APP,
RESPONSE_LATENCY,
TRANSACTIONS_PER_MINUTE,
} from '../constants/labels';
import { MobileFields } from './mobile_fields';
export function getMobileKPIConfig({ dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.KPI,
defaultSeriesType: 'line',
seriesTypes: ['line', 'bar', 'area'],
xAxisColumn: {
sourceField: '@timestamp',
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'median',
},
],
hasOperationType: true,
filterFields: [...Object.keys(MobileFields), LABEL_FIELDS_FILTER],
breakdownFields: Object.keys(MobileFields),
baseFilters: [
...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], dataView),
],
labels: {
...FieldLabels,
...MobileFields,
[TRANSACTION_DURATION]: RESPONSE_LATENCY,
[SERVICE_NAME]: MOBILE_APP,
[METRIC_SYSTEM_MEMORY_USAGE]: SYSTEM_MEMORY_USAGE,
[METRIC_SYSTEM_CPU_USAGE]: CPU_USAGE,
},
definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT],
metricOptions: [
{
label: RESPONSE_LATENCY,
field: TRANSACTION_DURATION,
id: TRANSACTION_DURATION,
columnType: OPERATION_COLUMN,
},
{
field: RECORDS_FIELD,
id: RECORDS_FIELD,
label: TRANSACTIONS_PER_MINUTE,
columnFilters: [
{
language: 'kuery',
query: `${PROCESSOR_EVENT}: transaction`,
},
],
timeScale: 'm',
},
{
label: SYSTEM_MEMORY_USAGE,
field: METRIC_SYSTEM_MEMORY_USAGE,
id: METRIC_SYSTEM_MEMORY_USAGE,
columnType: OPERATION_COLUMN,
columnFilters: [
{
language: 'kuery',
query: `${PROCESSOR_EVENT}: metric`,
},
],
},
{
label: CPU_USAGE,
field: METRIC_SYSTEM_CPU_USAGE,
id: METRIC_SYSTEM_CPU_USAGE,
columnType: OPERATION_COLUMN,
columnFilters: [
{
language: 'kuery',
query: `${PROCESSOR_EVENT}: metric`,
},
],
},
],
};
}

View file

@ -1,28 +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 {
CARRIER_LOCATION,
CARRIER_NAME,
CONNECTION_TYPE,
DEVICE_MODEL,
HOST_OS,
OS_PLATFORM,
SERVICE_VERSION,
URL_LABEL,
} from '../constants/labels';
export const MobileFields: Record<string, string> = {
'host.os.platform': OS_PLATFORM,
'host.os.full': HOST_OS,
'service.version': SERVICE_VERSION,
'network.carrier.icc': CARRIER_LOCATION,
'network.carrier.name': CARRIER_NAME,
'network.connection_type': CONNECTION_TYPE,
'labels.device_model': DEVICE_MODEL,
'url.full': URL_LABEL,
};

View file

@ -1,45 +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 { mockAppDataView, mockDataView } from '../../rtl_helpers';
import { LensAttributes } from '../lens_attributes';
import { METRIC_SYSTEM_MEMORY_USAGE, SERVICE_NAME } from '../constants/elasticsearch_fieldnames';
import { obsvReportConfigMap } from '../../obsv_exploratory_view';
import { testMobileKPIAttr } from '../test_data/mobile_test_attribute';
import { getLayerConfigs } from '../../hooks/use_lens_attributes';
import { DataViewState } from '../../hooks/use_app_data_view';
import { ReportTypes } from '../../../../..';
describe('Mobile kpi config test', function () {
mockAppDataView();
let lnsAttr: LensAttributes;
const layerConfigs = getLayerConfigs(
[
{
time: { from: 'now-15m', to: 'now' },
reportDefinitions: { [SERVICE_NAME]: ['ios-integration-testing'] },
selectedMetricField: METRIC_SYSTEM_MEMORY_USAGE,
color: 'green',
name: 'test-series',
dataType: 'mobile',
},
],
ReportTypes.KPI,
{} as any,
{ mobile: mockDataView } as DataViewState,
obsvReportConfigMap
);
beforeEach(() => {
lnsAttr = new LensAttributes(layerConfigs, ReportTypes.KPI);
});
it('should return expected json', function () {
expect(lnsAttr.getJSON()).toEqual(testMobileKPIAttr);
});
});

View file

@ -1,45 +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 { mockAppDataView, mockDataView } from '../../rtl_helpers';
import { getDefaultConfigs } from '../default_configs';
import { LayerConfig, LensAttributes } from '../lens_attributes';
import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv';
import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames';
import { obsvReportConfigMap } from '../../obsv_exploratory_view';
import { ReportTypes } from '../../../../..';
describe('Core web vital config test', function () {
mockAppDataView();
const seriesConfig = getDefaultConfigs({
reportType: ReportTypes.CORE_WEB_VITAL,
dataType: 'ux',
dataView: mockDataView,
reportConfigMap: obsvReportConfigMap,
});
let lnsAttr: LensAttributes;
const layerConfig: LayerConfig = {
seriesConfig,
color: 'green',
name: 'test-series',
breakdown: USER_AGENT_OS,
dataView: mockDataView,
time: { from: 'now-15m', to: 'now' },
reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] },
selectedMetricField: LCP_FIELD,
};
beforeEach(() => {
lnsAttr = new LensAttributes([layerConfig], ReportTypes.CORE_WEB_VITAL);
});
it('should return expected json', function () {
expect(lnsAttr.getJSON()).toEqual(sampleAttributeCoreWebVital);
});
});

View file

@ -1,161 +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 { euiPaletteForStatus } from '@elastic/eui';
import { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
FILTER_RECORDS,
LABEL_FIELDS_FILTER,
REPORT_METRIC_FIELD,
ReportTypes,
USE_BREAK_DOWN_COLUMN,
} from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
CLS_FIELD,
FID_FIELD,
LCP_FIELD,
PROCESSOR_EVENT,
SERVICE_NAME,
TRANSACTION_TYPE,
USER_AGENT_DEVICE,
USER_AGENT_NAME,
USER_AGENT_OS,
USER_AGENT_VERSION,
TRANSACTION_URL,
USER_AGENT_OS_VERSION,
URL_FULL,
SERVICE_ENVIRONMENT,
} from '../constants/elasticsearch_fieldnames';
import { CLS_LABEL, FID_LABEL, LCP_LABEL } from '../constants/labels';
export function getCoreWebVitalsConfig({ dataView }: ConfigProps): SeriesConfig {
const statusPallete = euiPaletteForStatus(3);
return {
defaultSeriesType: 'bar_horizontal_percentage_stacked',
reportType: ReportTypes.CORE_WEB_VITAL,
seriesTypes: ['bar_horizontal_percentage_stacked'],
xAxisColumn: {
sourceField: USE_BREAK_DOWN_COLUMN,
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
label: 'Good',
},
{
sourceField: REPORT_METRIC_FIELD,
label: 'Average',
},
{
sourceField: REPORT_METRIC_FIELD,
label: 'Poor',
},
],
hasOperationType: false,
filterFields: [
{
field: TRANSACTION_URL,
isNegated: false,
},
SERVICE_NAME,
{
field: USER_AGENT_OS,
nested: USER_AGENT_OS_VERSION,
},
CLIENT_GEO_COUNTRY_NAME,
USER_AGENT_DEVICE,
{
field: USER_AGENT_NAME,
nested: USER_AGENT_VERSION,
},
LABEL_FIELDS_FILTER,
],
breakdownFields: [
SERVICE_NAME,
USER_AGENT_NAME,
USER_AGENT_OS,
CLIENT_GEO_COUNTRY_NAME,
USER_AGENT_DEVICE,
URL_FULL,
],
baseFilters: [
...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', dataView),
...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', dataView),
],
labels: { ...FieldLabels, [SERVICE_NAME]: 'Web Application' },
definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT],
metricOptions: [
{
id: LCP_FIELD,
label: LCP_LABEL,
columnType: FILTER_RECORDS,
columnFilters: [
{
language: 'kuery',
query: `${LCP_FIELD} < 2500`,
},
{
language: 'kuery',
query: `${LCP_FIELD} > 2500 and ${LCP_FIELD} < 4000`,
},
{
language: 'kuery',
query: `${LCP_FIELD} > 4000`,
},
],
},
{
label: FID_LABEL,
id: FID_FIELD,
columnType: FILTER_RECORDS,
columnFilters: [
{
language: 'kuery',
query: `${FID_FIELD} < 100`,
},
{
language: 'kuery',
query: `${FID_FIELD} > 100 and ${FID_FIELD} < 300`,
},
{
language: 'kuery',
query: `${FID_FIELD} > 300`,
},
],
},
{
label: CLS_LABEL,
id: CLS_FIELD,
columnType: FILTER_RECORDS,
columnFilters: [
{
language: 'kuery',
query: `${CLS_FIELD} < 0.1`,
},
{
language: 'kuery',
query: `${CLS_FIELD} > 0.1 and ${CLS_FIELD} < 0.25`,
},
{
language: 'kuery',
query: `${CLS_FIELD} > 0.25`,
},
],
},
],
yConfig: [
{ color: statusPallete[0], forAccessor: 'y-axis-column' },
{ color: statusPallete[1], forAccessor: 'y-axis-column-1' },
{ color: statusPallete[2], forAccessor: 'y-axis-column-2' },
],
query: { query: 'transaction.type: "page-load"', language: 'kuery' },
};
}

View file

@ -1,115 +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 { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
REPORT_METRIC_FIELD,
RECORDS_PERCENTAGE_FIELD,
ReportTypes,
LABEL_FIELDS_FILTER,
} from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
CLS_FIELD,
FCP_FIELD,
FID_FIELD,
LCP_FIELD,
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TBT_FIELD,
TRANSACTION_DURATION,
TRANSACTION_TIME_TO_FIRST_BYTE,
TRANSACTION_TYPE,
TRANSACTION_URL,
USER_AGENT_DEVICE,
USER_AGENT_NAME,
USER_AGENT_OS,
USER_AGENT_VERSION,
} from '../constants/elasticsearch_fieldnames';
import {
BACKEND_TIME_LABEL,
CLS_LABEL,
FCP_LABEL,
FID_LABEL,
LCP_LABEL,
PAGE_LOAD_TIME_LABEL,
PAGES_LOADED_LABEL,
TBT_LABEL,
WEB_APPLICATION_LABEL,
} from '../constants/labels';
export function getRumDistributionConfig({ dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: 'line',
seriesTypes: [],
xAxisColumn: {
sourceField: REPORT_METRIC_FIELD,
},
yAxisColumns: [
{
sourceField: RECORDS_PERCENTAGE_FIELD,
label: PAGES_LOADED_LABEL,
},
],
hasOperationType: false,
filterFields: [
{
field: TRANSACTION_URL,
isNegated: false,
},
USER_AGENT_OS,
CLIENT_GEO_COUNTRY_NAME,
USER_AGENT_DEVICE,
{
field: USER_AGENT_NAME,
nested: USER_AGENT_VERSION,
},
LABEL_FIELDS_FILTER,
],
breakdownFields: [
USER_AGENT_NAME,
USER_AGENT_OS,
CLIENT_GEO_COUNTRY_NAME,
USER_AGENT_DEVICE,
SERVICE_NAME,
],
definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT],
metricOptions: [
{
label: PAGE_LOAD_TIME_LABEL,
id: TRANSACTION_DURATION,
field: TRANSACTION_DURATION,
showPercentileAnnotations: true,
},
{
label: BACKEND_TIME_LABEL,
id: TRANSACTION_TIME_TO_FIRST_BYTE,
field: TRANSACTION_TIME_TO_FIRST_BYTE,
},
{ label: FCP_LABEL, id: FCP_FIELD, field: FCP_FIELD },
{ label: TBT_LABEL, id: TBT_FIELD, field: TBT_FIELD },
{ label: LCP_LABEL, id: LCP_FIELD, field: LCP_FIELD },
{ label: FID_LABEL, id: FID_FIELD, field: FID_FIELD },
{ label: CLS_LABEL, id: CLS_FIELD, field: CLS_FIELD },
],
baseFilters: [
...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', dataView),
...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', dataView),
],
labels: {
...FieldLabels,
[SERVICE_NAME]: WEB_APPLICATION_LABEL,
[TRANSACTION_DURATION]: PAGE_LOAD_TIME_LABEL,
},
// rum page load transactions are always less then 60 seconds
query: { query: 'transaction.duration.us < 60000000', language: 'kuery' },
};
}

View file

@ -1,92 +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 { FieldFormat } from '../../types';
import {
FCP_FIELD,
FID_FIELD,
LCP_FIELD,
TBT_FIELD,
TRANSACTION_DURATION,
TRANSACTION_TIME_TO_FIRST_BYTE,
} from '../constants/elasticsearch_fieldnames';
export const rumFieldFormats: FieldFormat[] = [
{
field: TRANSACTION_DURATION,
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'asSeconds',
showSuffix: true,
outputPrecision: 1,
useShortSuffix: true,
},
},
},
{
field: FCP_FIELD,
format: {
id: 'duration',
params: {
inputFormat: 'milliseconds',
outputFormat: 'humanizePrecise',
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: LCP_FIELD,
format: {
id: 'duration',
params: {
inputFormat: 'milliseconds',
outputFormat: 'humanizePrecise',
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: TBT_FIELD,
format: {
id: 'duration',
params: {
inputFormat: 'milliseconds',
outputFormat: 'humanizePrecise',
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: FID_FIELD,
format: {
id: 'duration',
params: {
inputFormat: 'milliseconds',
outputFormat: 'humanizePrecise',
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: TRANSACTION_TIME_TO_FIRST_BYTE,
format: {
id: 'duration',
params: {
inputFormat: 'milliseconds',
outputFormat: 'humanizePrecise',
showSuffix: true,
useShortSuffix: true,
},
},
},
];

View file

@ -1,112 +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 { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
LABEL_FIELDS_BREAKDOWN,
LABEL_FIELDS_FILTER,
OPERATION_COLUMN,
RECORDS_FIELD,
REPORT_METRIC_FIELD,
PERCENTILE,
ReportTypes,
} from '../constants';
import { buildPhraseFilter } from '../utils';
import {
CLIENT_GEO_COUNTRY_NAME,
CLS_FIELD,
FCP_FIELD,
FID_FIELD,
LCP_FIELD,
PROCESSOR_EVENT,
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TBT_FIELD,
TRANSACTION_DURATION,
TRANSACTION_TYPE,
USER_AGENT_DEVICE,
USER_AGENT_NAME,
USER_AGENT_OS,
USER_AGENT_VERSION,
TRANSACTION_TIME_TO_FIRST_BYTE,
TRANSACTION_URL,
} from '../constants/elasticsearch_fieldnames';
import {
BACKEND_TIME_LABEL,
CLS_LABEL,
FCP_LABEL,
FID_LABEL,
LCP_LABEL,
PAGE_LOAD_TIME_LABEL,
PAGE_VIEWS_LABEL,
TBT_LABEL,
WEB_APPLICATION_LABEL,
} from '../constants/labels';
export function getKPITrendsLensConfig({ dataView }: ConfigProps): SeriesConfig {
return {
defaultSeriesType: 'bar_stacked',
seriesTypes: [],
reportType: ReportTypes.KPI,
xAxisColumn: {
sourceField: '@timestamp',
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'median',
},
],
hasOperationType: false,
filterFields: [
TRANSACTION_URL,
USER_AGENT_OS,
CLIENT_GEO_COUNTRY_NAME,
USER_AGENT_DEVICE,
{
field: USER_AGENT_NAME,
nested: USER_AGENT_VERSION,
},
LABEL_FIELDS_FILTER,
],
breakdownFields: [
USER_AGENT_NAME,
USER_AGENT_OS,
CLIENT_GEO_COUNTRY_NAME,
USER_AGENT_DEVICE,
PERCENTILE,
LABEL_FIELDS_BREAKDOWN,
],
baseFilters: [
...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', dataView),
...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', dataView),
],
labels: { ...FieldLabels, [SERVICE_NAME]: WEB_APPLICATION_LABEL },
definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT],
metricOptions: [
{ field: RECORDS_FIELD, id: RECORDS_FIELD, label: PAGE_VIEWS_LABEL },
{
label: PAGE_LOAD_TIME_LABEL,
field: TRANSACTION_DURATION,
id: TRANSACTION_DURATION,
columnType: OPERATION_COLUMN,
},
{
label: BACKEND_TIME_LABEL,
field: TRANSACTION_TIME_TO_FIRST_BYTE,
id: TRANSACTION_TIME_TO_FIRST_BYTE,
columnType: OPERATION_COLUMN,
},
{ label: FCP_LABEL, field: FCP_FIELD, id: FCP_FIELD, columnType: OPERATION_COLUMN },
{ label: TBT_LABEL, field: TBT_FIELD, id: TBT_FIELD, columnType: OPERATION_COLUMN },
{ label: LCP_LABEL, field: LCP_FIELD, id: LCP_FIELD, columnType: OPERATION_COLUMN },
{ label: FID_LABEL, field: FID_FIELD, id: FID_FIELD, columnType: OPERATION_COLUMN },
{ label: CLS_LABEL, field: CLS_FIELD, id: CLS_FIELD, columnType: OPERATION_COLUMN },
],
};
}

View file

@ -1,56 +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 { ConfigProps, SeriesConfig } from '../../types';
import { FieldLabels } from '../constants';
import { buildPhraseFilter } from '../utils';
import { PROCESSOR_EVENT, TRANSACTION_TYPE } from '../constants/elasticsearch_fieldnames';
export function getSingleMetricConfig({ dataView }: ConfigProps): SeriesConfig {
return {
defaultSeriesType: 'line',
xAxisColumn: {},
yAxisColumns: [
{
operationType: 'median',
},
],
breakdownFields: [],
filterFields: [],
seriesTypes: [],
hasOperationType: true,
definitionFields: ['service.name'],
reportType: 'single-metric',
baseFilters: [
...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', dataView),
...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', dataView),
],
metricOptions: [
{
id: 'page_views',
field: 'Records',
label: 'Total page views',
},
{
id: 'page_load_time',
field: 'transaction.duration.us',
label: 'Page load time',
},
{
id: 'backend_time',
field: 'transaction.marks.agent.timeToFirstByte',
label: 'Backend time',
},
{
id: 'frontend_time',
field: 'transaction.marks.agent.timeToFirstByte',
label: 'Frontend time',
},
],
labels: FieldLabels,
};
}

View file

@ -1,97 +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 { ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
REPORT_METRIC_FIELD,
RECORDS_PERCENTAGE_FIELD,
ReportTypes,
} from '../constants';
import {
CLS_LABEL,
DCL_LABEL,
DOCUMENT_ONLOAD_LABEL,
FCP_LABEL,
LCP_LABEL,
MONITORS_DURATION_LABEL,
PINGS_LABEL,
} from '../constants/labels';
import {
MONITOR_DURATION_US,
SYNTHETICS_CLS,
SYNTHETICS_DCL,
SYNTHETICS_DOCUMENT_ONLOAD,
SYNTHETICS_FCP,
SYNTHETICS_LCP,
} from '../constants/field_names/synthetics';
import { buildExistsFilter } from '../utils';
export function getSyntheticsDistributionConfig({ series, dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.DISTRIBUTION,
defaultSeriesType: series?.seriesType || 'line',
seriesTypes: [],
xAxisColumn: {
sourceField: REPORT_METRIC_FIELD,
},
yAxisColumns: [
{
sourceField: RECORDS_PERCENTAGE_FIELD,
label: PINGS_LABEL,
},
],
hasOperationType: false,
filterFields: ['monitor.type', 'observer.geo.name', 'tags', 'url.full'],
breakdownFields: [
'observer.geo.name',
'monitor.name',
'monitor.id',
'monitor.type',
'tags',
'url.port',
],
baseFilters: [],
definitionFields: [
{ field: 'monitor.name', nested: 'synthetics.step.name.keyword', singleSelection: true },
{ field: 'url.full', filters: buildExistsFilter('summary.up', dataView) },
],
metricOptions: [
{
label: MONITORS_DURATION_LABEL,
id: MONITOR_DURATION_US,
field: MONITOR_DURATION_US,
},
{
label: LCP_LABEL,
field: SYNTHETICS_LCP,
id: SYNTHETICS_LCP,
},
{
label: FCP_LABEL,
field: SYNTHETICS_FCP,
id: SYNTHETICS_FCP,
},
{
label: DCL_LABEL,
field: SYNTHETICS_DCL,
id: SYNTHETICS_DCL,
},
{
label: DOCUMENT_ONLOAD_LABEL,
field: SYNTHETICS_DOCUMENT_ONLOAD,
id: SYNTHETICS_DOCUMENT_ONLOAD,
},
{
label: CLS_LABEL,
field: SYNTHETICS_CLS,
id: SYNTHETICS_CLS,
},
],
labels: { ...FieldLabels, 'monitor.duration.us': MONITORS_DURATION_LABEL },
};
}

View file

@ -1,104 +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 { FieldFormat } from '../../types';
import {
SYNTHETICS_DCL,
SYNTHETICS_DOCUMENT_ONLOAD,
SYNTHETICS_FCP,
SYNTHETICS_LCP,
SYNTHETICS_STEP_DURATION,
} from '../constants/field_names/synthetics';
export const MS_TO_HUMANIZE_PRECISE = {
inputFormat: 'milliseconds' as const,
outputFormat: 'humanizePrecise' as const,
outputPrecision: 1,
showSuffix: true,
useShortSuffix: true,
};
export const syntheticsFieldFormats: FieldFormat[] = [
{
field: 'monitor.duration.us',
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'humanizePrecise',
outputPrecision: 1,
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: SYNTHETICS_STEP_DURATION,
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'humanizePrecise',
outputPrecision: 1,
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: SYNTHETICS_LCP,
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'humanizePrecise',
outputPrecision: 1,
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: SYNTHETICS_FCP,
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'humanizePrecise',
outputPrecision: 1,
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: SYNTHETICS_DOCUMENT_ONLOAD,
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'humanizePrecise',
outputPrecision: 1,
showSuffix: true,
useShortSuffix: true,
},
},
},
{
field: SYNTHETICS_DCL,
format: {
id: 'duration',
params: {
inputFormat: 'microseconds',
outputFormat: 'humanizePrecise',
outputPrecision: 1,
showSuffix: true,
useShortSuffix: true,
},
},
},
];

View file

@ -1,50 +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 { ConfigProps, SeriesConfig } from '../../types';
import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants';
import { DOWN_LABEL, UP_LABEL } from '../constants/labels';
import { SYNTHETICS_STEP_NAME } from '../constants/field_names/synthetics';
import { buildExistsFilter } from '../utils';
const SUMMARY_UP = 'summary.up';
const SUMMARY_DOWN = 'summary.down';
export function getSyntheticsHeatmapConfig({ dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.HEATMAP,
defaultSeriesType: 'bar_stacked',
seriesTypes: [],
xAxisColumn: {
sourceField: '@timestamp',
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'median',
},
],
hasOperationType: false,
filterFields: ['observer.geo.name', 'monitor.type', 'tags', 'url.full'],
breakdownFields: ['observer.geo.name', 'monitor.type', 'monitor.name', SYNTHETICS_STEP_NAME],
baseFilters: [],
definitionFields: [
{ field: 'monitor.name' },
{ field: 'url.full', filters: buildExistsFilter('summary.up', dataView) },
],
metricOptions: [
{
label: 'Failed tests',
id: 'failed_tests',
columnFilter: { language: 'kuery', query: 'summary.down > 0' },
format: 'number',
field: RECORDS_FIELD,
},
],
labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL },
};
}

View file

@ -1,222 +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 { ColumnFilter, ConfigProps, SeriesConfig } from '../../types';
import {
FieldLabels,
OPERATION_COLUMN,
REPORT_METRIC_FIELD,
PERCENTILE,
ReportTypes,
FORMULA_COLUMN,
} from '../constants';
import {
CLS_LABEL,
DCL_LABEL,
DOWN_LABEL,
FCP_LABEL,
LCP_LABEL,
MONITORS_DURATION_LABEL,
STEP_DURATION_LABEL,
UP_LABEL,
PAGE_LOAD_TIME_LABEL,
NETWORK_TIMINGS_LABEL,
} from '../constants/labels';
import {
MONITOR_DURATION_US,
NETWORK_TIMINGS_FIELDS,
SYNTHETICS_CLS,
SYNTHETICS_DCL,
SYNTHETICS_DOCUMENT_ONLOAD,
SYNTHETICS_FCP,
SYNTHETICS_LCP,
SYNTHETICS_STEP_DURATION,
SYNTHETICS_STEP_NAME,
} from '../constants/field_names/synthetics';
import { buildExistsFilter } from '../utils';
const SUMMARY_UP = 'summary.up';
const SUMMARY_DOWN = 'summary.down';
export const isStepLevelMetric = (metric?: string) => {
if (!metric) {
return false;
}
return [
SYNTHETICS_LCP,
SYNTHETICS_FCP,
SYNTHETICS_CLS,
SYNTHETICS_DCL,
SYNTHETICS_STEP_DURATION,
SYNTHETICS_DOCUMENT_ONLOAD,
].includes(metric);
};
export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig {
return {
reportType: ReportTypes.KPI,
defaultSeriesType: 'bar_stacked',
seriesTypes: [],
xAxisColumn: {
sourceField: '@timestamp',
},
yAxisColumns: [
{
sourceField: REPORT_METRIC_FIELD,
operationType: 'median',
},
],
hasOperationType: true,
filterFields: ['observer.geo.name', 'monitor.type', 'tags', 'url.full'],
breakdownFields: [
'observer.geo.name',
'monitor.type',
'monitor.name',
SYNTHETICS_STEP_NAME,
PERCENTILE,
],
baseFilters: [],
definitionFields: [
{ field: 'monitor.name', nested: SYNTHETICS_STEP_NAME, singleSelection: true },
{ field: 'url.full', filters: buildExistsFilter('summary.up', dataView) },
],
metricOptions: [
{
label: MONITORS_DURATION_LABEL,
field: MONITOR_DURATION_US,
id: MONITOR_DURATION_US,
columnType: OPERATION_COLUMN,
},
{
label: 'Monitor availability',
id: 'monitor_availability',
columnType: FORMULA_COLUMN,
formula: "1- (count(kql='summary.down > 0') / count(kql='summary: *'))",
},
{
label: 'Monitor Errors',
id: 'monitor_errors',
columnType: OPERATION_COLUMN,
field: 'state.id',
columnFilters: [
{
language: 'kuery',
query: `summary.down > 0`,
},
],
},
{
label: 'Monitor Complete',
id: 'state.up',
field: 'state.up',
columnType: OPERATION_COLUMN,
columnFilters: [
{
language: 'kuery',
query: `summary: * and summary.down: 0 and monitor.status: "up"`,
},
],
},
{
label: 'Total runs',
id: 'monitor.check_group',
field: 'monitor.check_group',
columnType: OPERATION_COLUMN,
columnFilters: [
{
language: 'kuery',
query: `summary: *`,
},
],
},
{
field: SUMMARY_UP,
id: SUMMARY_UP,
label: UP_LABEL,
columnType: OPERATION_COLUMN,
palette: { type: 'palette', name: 'status' },
},
{
field: SUMMARY_DOWN,
id: SUMMARY_DOWN,
label: DOWN_LABEL,
columnType: OPERATION_COLUMN,
palette: { type: 'palette', name: 'status' },
},
{
label: STEP_DURATION_LABEL,
field: SYNTHETICS_STEP_DURATION,
id: SYNTHETICS_STEP_DURATION,
columnType: OPERATION_COLUMN,
columnFilters: [STEP_END_FILTER],
},
{
label: LCP_LABEL,
field: SYNTHETICS_LCP,
id: SYNTHETICS_LCP,
columnType: OPERATION_COLUMN,
columnFilters: getStepMetricColumnFilter(SYNTHETICS_LCP),
},
{
label: FCP_LABEL,
field: SYNTHETICS_FCP,
id: SYNTHETICS_FCP,
columnType: OPERATION_COLUMN,
columnFilters: getStepMetricColumnFilter(SYNTHETICS_FCP),
},
{
label: DCL_LABEL,
field: SYNTHETICS_DCL,
id: SYNTHETICS_DCL,
columnType: OPERATION_COLUMN,
columnFilters: getStepMetricColumnFilter(SYNTHETICS_DCL),
},
{
label: PAGE_LOAD_TIME_LABEL,
field: SYNTHETICS_DOCUMENT_ONLOAD,
id: SYNTHETICS_DOCUMENT_ONLOAD,
columnType: OPERATION_COLUMN,
columnFilters: getStepMetricColumnFilter(SYNTHETICS_DOCUMENT_ONLOAD),
},
{
label: CLS_LABEL,
field: SYNTHETICS_CLS,
id: SYNTHETICS_CLS,
columnType: OPERATION_COLUMN,
columnFilters: getStepMetricColumnFilter(SYNTHETICS_CLS),
},
{
label: NETWORK_TIMINGS_LABEL,
id: 'network_timings',
columnType: OPERATION_COLUMN,
items: NETWORK_TIMINGS_FIELDS.map((field) => ({
label: FieldLabels[field] ?? field,
field,
id: field,
columnType: OPERATION_COLUMN,
columnFilters: getStepMetricColumnFilter(field, 'journey/network_info'),
})),
},
],
labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL },
};
}
const getStepMetricColumnFilter = (
field: string,
stepType: 'step/metrics' | 'step/end' | 'journey/network_info' = 'step/metrics'
): ColumnFilter[] => {
return [
{
language: 'kuery',
query: `synthetics.type: ${stepType} and ${field}: * and ${field} > 0`,
},
];
};
const STEP_END_FILTER: ColumnFilter = {
language: 'kuery',
query: `synthetics.type: step/end`,
};

View file

@ -1,62 +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 { RuntimeField } from '@kbn/data-views-plugin/public';
import { MS_TO_HUMANIZE_PRECISE } from './field_formats';
import {
SYNTHETICS_DNS_TIMINGS,
SYNTHETICS_BLOCKED_TIMINGS,
SYNTHETICS_CONNECT_TIMINGS,
SYNTHETICS_TOTAL_TIMINGS,
SYNTHETICS_RECEIVE_TIMINGS,
SYNTHETICS_SEND_TIMINGS,
SYNTHETICS_WAIT_TIMINGS,
SYNTHETICS_SSL_TIMINGS,
} from '../constants/field_names/synthetics';
const LONG_FIELD = {
type: 'long' as const,
format: {
id: 'duration',
params: MS_TO_HUMANIZE_PRECISE,
},
};
export const syntheticsRuntimeFields: Array<{ name: string; field: RuntimeField }> = [
{
name: SYNTHETICS_DNS_TIMINGS,
field: LONG_FIELD,
},
{
name: SYNTHETICS_BLOCKED_TIMINGS,
field: LONG_FIELD,
},
{
name: SYNTHETICS_CONNECT_TIMINGS,
field: LONG_FIELD,
},
{
name: SYNTHETICS_TOTAL_TIMINGS,
field: LONG_FIELD,
},
{
name: SYNTHETICS_RECEIVE_TIMINGS,
field: LONG_FIELD,
},
{
name: SYNTHETICS_SEND_TIMINGS,
field: LONG_FIELD,
},
{
name: SYNTHETICS_WAIT_TIMINGS,
field: LONG_FIELD,
},
{
name: SYNTHETICS_SSL_TIMINGS,
field: LONG_FIELD,
},
];

View file

@ -1,178 +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 { LegacyMetricState } from '@kbn/lens-plugin/common';
import { euiPaletteForStatus } from '@elastic/eui';
import {
SYNTHETICS_STEP_DURATION,
SYNTHETICS_STEP_NAME,
} from '../constants/field_names/synthetics';
import { ConfigProps, SeriesConfig } from '../../types';
import { FieldLabels, FORMULA_COLUMN, RECORDS_FIELD } from '../constants';
import { buildExistsFilter } from '../utils';
export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): SeriesConfig {
return {
defaultSeriesType: 'line',
xAxisColumn: {},
yAxisColumns: [
{
operationType: 'median',
},
],
breakdownFields: [],
filterFields: [],
seriesTypes: [],
hasOperationType: true,
definitionFields: [
{ field: 'monitor.name', nested: SYNTHETICS_STEP_NAME, singleSelection: true },
{ field: 'url.full', filters: buildExistsFilter('summary.up', dataView) },
],
reportType: 'single-metric',
baseFilters: [],
metricOptions: [
{
id: 'monitor_availability',
columnType: FORMULA_COLUMN,
label: 'Availability',
formula: "1- (count(kql='summary.down > 0') / count())",
metricStateOptions: {
colorMode: 'Labels',
palette: {
name: 'custom',
type: 'palette',
params: {
steps: 3,
name: 'custom',
reverse: false,
rangeType: 'number',
rangeMin: 0,
rangeMax: 1,
progression: 'fixed',
stops: [
{ color: '#cc5642', stop: 0.9 },
{ color: '#d6bf57', stop: 0.95 },
{ color: '#209280', stop: 1.9903347477604902 },
],
colorStops: [
{ color: '#cc5642', stop: 0.8 },
{ color: '#d6bf57', stop: 0.9 },
{ color: '#209280', stop: 0.95 },
],
continuity: 'above',
maxSteps: 5,
},
},
titlePosition: 'bottom',
},
columnFilter: { language: 'kuery', query: 'summary.up: *' },
},
{
id: 'monitor_duration',
field: 'monitor.duration.us',
label: i18n.translate('xpack.observability.expView.avgDuration', {
defaultMessage: 'Avg. Duration',
}),
metricStateOptions: {
titlePosition: 'bottom',
},
columnFilter: { language: 'kuery', query: 'summary.up: *' },
},
{
id: 'step_duration',
field: SYNTHETICS_STEP_DURATION,
label: i18n.translate('xpack.observability.expView.stepDuration', {
defaultMessage: 'Total step duration',
}),
metricStateOptions: {
titlePosition: 'bottom',
textAlign: 'center',
},
},
{
id: 'monitor_total_runs',
label: i18n.translate('xpack.observability.expView.totalRuns', {
defaultMessage: 'Total Runs',
}),
metricStateOptions: {
titlePosition: 'bottom',
},
columnType: FORMULA_COLUMN,
formula: "unique_count(monitor.check_group, kql='summary: *')",
format: 'number',
},
{
id: 'monitor_complete',
label: i18n.translate('xpack.observability.expView.complete', {
defaultMessage: 'Complete',
}),
metricStateOptions: {
titlePosition: 'bottom',
},
columnType: FORMULA_COLUMN,
formula: 'unique_count(monitor.check_group, kql=\'monitor.status: "up"\')',
format: 'number',
},
{
id: 'monitor_errors',
label: i18n.translate('xpack.observability.expView.errors', {
defaultMessage: 'Errors',
}),
metricStateOptions: {
titlePosition: 'bottom',
colorMode: 'Labels',
palette: getColorPalette('danger'),
},
columnType: FORMULA_COLUMN,
formula: 'unique_count(state.id, kql=\'monitor.status: "down"\')',
format: 'number',
},
{
id: 'monitor_failed_tests',
label: i18n.translate('xpack.observability.expView.failedTests', {
defaultMessage: 'Failed tests',
}),
metricStateOptions: {
titlePosition: 'bottom',
},
field: RECORDS_FIELD,
format: 'number',
columnFilter: { language: 'kuery', query: 'summary.down > 0' },
},
],
labels: FieldLabels,
};
}
export const getColorPalette = (
color: 'danger' | 'warning' | 'success' | string
): LegacyMetricState['palette'] => {
const statusPalette = euiPaletteForStatus(5);
let valueColor = color ?? statusPalette[3];
if (color === 'danger') {
valueColor = statusPalette[3];
}
return {
name: 'custom',
type: 'palette',
params: {
steps: 3,
name: 'custom',
reverse: false,
rangeType: 'number',
rangeMin: 0,
progression: 'fixed',
stops: [{ color: valueColor, stop: 100 }],
colorStops: [{ color: valueColor, stop: 0 }],
continuity: 'above',
maxSteps: 5,
},
};
};

View file

@ -1,98 +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 { mockDataView } from '../../rtl_helpers';
export const testMobileKPIAttr = {
title: 'Prefilled from exploratory view app',
description: '',
references: [],
visualizationType: 'lnsXY',
state: {
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
],
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0-0'],
columns: {
'x-axis-column-layer0': {
sourceField: '@timestamp',
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: { interval: 'auto', includeEmptyRows: true },
scale: 'interval',
},
'y-axis-column-layer0-0': {
isBucketed: false,
label: 'test-series',
operationType: 'median',
params: {},
scale: 'ratio',
sourceField: 'system.memory.usage',
dataType: 'number',
filter: {
query:
'service.name: "ios-integration-testing" and agent.name: (iOS/swift or open-telemetry/swift) and processor.event: metric',
language: 'kuery',
},
},
},
incompleteColumns: {},
},
},
},
},
visualization: {
legend: {
isVisible: true,
showSingleSeries: true,
position: 'right',
legendSize: 'auto',
shouldTruncate: false,
},
valueLabels: 'hide',
fittingFunction: 'Linear',
curveType: 'CURVE_MONOTONE_X',
axisTitlesVisibilitySettings: { x: false, yLeft: true, yRight: true },
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
gridlinesVisibilitySettings: { x: false, yLeft: true, yRight: true },
preferredSeriesType: 'line',
layers: [
{
accessors: ['y-axis-column-layer0-0'],
layerId: 'layer0',
layerType: 'data',
palette: undefined,
seriesType: 'line',
yConfig: [{ forAccessor: 'y-axis-column-layer0-0', color: 'green', axisMode: 'left' }],
xAccessor: 'x-axis-column-layer0',
},
],
},
query: {
query:
'service.name: "ios-integration-testing" and agent.name: (iOS/swift or open-telemetry/swift)',
language: 'kuery',
},
filters: [],
},
};

View file

@ -1,342 +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 { mockDataView } from '../../rtl_helpers';
import { RECORDS_FIELD } from '../constants';
export const sampleAttribute = {
description: '',
references: [],
state: {
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0-reference-lines',
type: 'index-pattern',
},
],
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: [
'x-axis-column-layer0',
'y-axis-column-layer0-0',
'y-axis-column-layer0X0',
'y-axis-column-layer0X1',
'y-axis-column-layer0X2',
'y-axis-column-layer0X3',
],
columns: {
'x-axis-column-layer0': {
dataType: 'number',
isBucketed: true,
label: 'Page load time',
operationType: 'range',
params: {
maxBars: 'auto',
ranges: [
{
from: 0,
label: '',
to: 1000,
},
],
type: 'histogram',
},
scale: 'interval',
sourceField: 'transaction.duration.us',
},
'y-axis-column-layer0-0': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
isBucketed: false,
label: 'test-series',
operationType: 'formula',
params: {
format: {
id: 'percent',
params: {
decimals: 0,
},
},
formula:
"count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *') / overall_sum(count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *'))",
isFormulaBroken: false,
},
references: ['y-axis-column-layer0X3'],
},
'y-axis-column-layer0X0': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: RECORDS_FIELD,
timeScale: undefined,
timeShift: undefined,
},
'y-axis-column-layer0X1': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : *',
},
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: RECORDS_FIELD,
timeScale: undefined,
timeShift: undefined,
},
'y-axis-column-layer0X2': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'overall_sum',
params: undefined,
references: ['y-axis-column-layer0X1'],
scale: 'ratio',
},
'y-axis-column-layer0X3': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'math',
params: {
tinymathAst: {
args: ['y-axis-column-layer0X0', 'y-axis-column-layer0X2'],
location: {
max: 212,
min: 0,
},
name: 'divide',
text: "count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *') / overall_sum(count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : *'))",
type: 'function',
},
},
references: ['y-axis-column-layer0X0', 'y-axis-column-layer0X2'],
scale: 'ratio',
},
},
incompleteColumns: {},
},
'layer0-reference-lines': {
columnOrder: [
'50th-percentile-reference-line-layer0-reference-lines',
'75th-percentile-reference-line-layer0-reference-lines',
'90th-percentile-reference-line-layer0-reference-lines',
'95th-percentile-reference-line-layer0-reference-lines',
'99th-percentile-reference-line-layer0-reference-lines',
],
columns: {
'50th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '50th',
operationType: 'percentile',
params: {
percentile: 50,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'75th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '75th',
operationType: 'percentile',
params: {
percentile: 75,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'90th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '90th',
operationType: 'percentile',
params: {
percentile: 90,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'95th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '95th',
operationType: 'percentile',
params: {
percentile: 95,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'99th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '99th',
operationType: 'percentile',
params: {
percentile: 99,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
},
incompleteColumns: {},
},
},
},
},
filters: [],
query: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : * and transaction.duration.us < 60000000',
},
visualization: {
axisTitlesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
curveType: 'CURVE_MONOTONE_X',
fittingFunction: 'Linear',
gridlinesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
layers: [
{
accessors: ['y-axis-column-layer0-0'],
layerId: 'layer0',
layerType: 'data',
palette: undefined,
seriesType: 'line',
xAccessor: 'x-axis-column-layer0',
yConfig: [
{
color: 'green',
forAccessor: 'y-axis-column-layer0-0',
axisMode: 'left',
},
],
},
{
accessors: [
'50th-percentile-reference-line-layer0-reference-lines',
'75th-percentile-reference-line-layer0-reference-lines',
'90th-percentile-reference-line-layer0-reference-lines',
'95th-percentile-reference-line-layer0-reference-lines',
'99th-percentile-reference-line-layer0-reference-lines',
],
layerId: 'layer0-reference-lines',
layerType: 'referenceLine',
yConfig: [
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '50th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '75th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '90th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '95th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '99th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
],
},
],
legend: {
isVisible: true,
position: 'right',
showSingleSeries: true,
legendSize: 'auto',
shouldTruncate: false,
},
preferredSeriesType: 'line',
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
valueLabels: 'hide',
},
},
title: 'Prefilled from exploratory view app',
visualizationType: 'lnsXY',
};

View file

@ -1,161 +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 { mockDataView } from '../../rtl_helpers';
import { RECORDS_FIELD } from '../constants';
export const sampleAttributeCoreWebVital = {
description: '',
references: [],
state: {
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
],
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: [
'x-axis-column-layer0',
'y-axis-column-layer0-0',
'y-axis-column-1',
'y-axis-column-2',
],
columns: {
'x-axis-column-layer0': {
dataType: 'string',
isBucketed: true,
label: 'Operating system',
operationType: 'terms',
params: {
missingBucket: false,
orderBy: {
columnId: 'y-axis-column-layer0-0',
type: 'column',
},
orderDirection: 'desc',
otherBucket: true,
size: 10,
},
scale: 'ordinal',
sourceField: 'user_agent.os.name',
},
'y-axis-column-1': {
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.marks.agent.largestContentfulPaint > 2500 and transaction.marks.agent.largestContentfulPaint < 4000',
},
isBucketed: false,
label: 'Average',
operationType: 'count',
scale: 'ratio',
sourceField: RECORDS_FIELD,
},
'y-axis-column-2': {
dataType: 'number',
filter: {
language: 'kuery',
query: 'transaction.marks.agent.largestContentfulPaint > 4000',
},
isBucketed: false,
label: 'Poor',
operationType: 'count',
scale: 'ratio',
sourceField: RECORDS_FIELD,
},
'y-axis-column-layer0-0': {
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.marks.agent.largestContentfulPaint < 2500',
},
isBucketed: false,
label: 'Good',
operationType: 'count',
scale: 'ratio',
sourceField: RECORDS_FIELD,
},
},
incompleteColumns: {},
},
},
},
},
filters: [],
query: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type: "page-load"',
},
visualization: {
axisTitlesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
curveType: 'CURVE_MONOTONE_X',
fittingFunction: 'Linear',
gridlinesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
layers: [
{
accessors: ['y-axis-column-layer0-0', 'y-axis-column-1', 'y-axis-column-2'],
layerId: 'layer0',
layerType: 'data',
palette: undefined,
seriesType: 'bar_horizontal_percentage_stacked',
xAccessor: 'x-axis-column-layer0',
yConfig: [
{
color: '#209280',
forAccessor: 'y-axis-column',
},
{
color: '#d6bf57',
forAccessor: 'y-axis-column-1',
},
{
color: '#cc5642',
forAccessor: 'y-axis-column-2',
},
],
},
],
legend: {
isVisible: true,
showSingleSeries: true,
position: 'right',
shouldTruncate: false,
legendSize: 'auto',
},
preferredSeriesType: 'line',
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
valueLabels: 'hide',
},
},
title: 'Prefilled from exploratory view app',
visualizationType: 'lnsXY',
};

View file

@ -1,116 +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 { mockDataView } from '../../rtl_helpers';
import { RECORDS_FIELD } from '../constants';
export const sampleAttributeKpi = {
description: '',
references: [],
state: {
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
],
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0-0'],
columns: {
'x-axis-column-layer0': {
dataType: 'date',
isBucketed: true,
label: '@timestamp',
operationType: 'date_histogram',
params: {
interval: 'auto',
includeEmptyRows: true,
},
scale: 'interval',
sourceField: '@timestamp',
},
'y-axis-column-layer0-0': {
dataType: 'number',
filter: {
language: 'kuery',
query: 'transaction.type: page-load and processor.event: transaction',
},
isBucketed: false,
label: 'test-series',
operationType: 'count',
scale: 'ratio',
sourceField: RECORDS_FIELD,
},
},
incompleteColumns: {},
},
},
},
},
filters: [],
query: {
language: 'kuery',
query: 'transaction.type: page-load and processor.event: transaction',
},
visualization: {
axisTitlesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
curveType: 'CURVE_MONOTONE_X',
fittingFunction: 'Linear',
gridlinesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
layers: [
{
accessors: ['y-axis-column-layer0-0'],
layerId: 'layer0',
layerType: 'data',
palette: undefined,
seriesType: 'line',
xAccessor: 'x-axis-column-layer0',
yConfig: [
{
color: 'green',
forAccessor: 'y-axis-column-layer0-0',
axisMode: 'left',
},
],
},
],
legend: {
isVisible: true,
showSingleSeries: true,
position: 'right',
legendSize: 'auto',
shouldTruncate: false,
},
preferredSeriesType: 'line',
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
valueLabels: 'hide',
},
},
title: 'Prefilled from exploratory view app',
visualizationType: 'lnsXY',
};

View file

@ -1,342 +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 { mockDataView } from '../../rtl_helpers';
import { RECORDS_FIELD } from '../constants';
export const sampleAttributeWithReferenceLines = {
description: '',
references: [],
state: {
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0-reference-lines',
type: 'index-pattern',
},
],
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: [
'x-axis-column-layer0',
'y-axis-column-layer0-0',
'y-axis-column-layer0X0',
'y-axis-column-layer0X1',
'y-axis-column-layer0X2',
'y-axis-column-layer0X3',
],
columns: {
'x-axis-column-layer0': {
dataType: 'number',
isBucketed: true,
label: 'Page load time',
operationType: 'range',
params: {
maxBars: 'auto',
ranges: [
{
from: 0,
label: '',
to: 1000,
},
],
type: 'histogram',
},
scale: 'interval',
sourceField: 'transaction.duration.us',
},
'y-axis-column-layer0-0': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)',
},
isBucketed: false,
label: 'test-series',
operationType: 'formula',
params: {
format: {
id: 'percent',
params: {
decimals: 0,
},
},
formula:
"count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)') / overall_sum(count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)'))",
isFormulaBroken: false,
},
references: ['y-axis-column-layer0X3'],
},
'y-axis-column-layer0X0': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)',
},
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: RECORDS_FIELD,
timeScale: undefined,
timeShift: undefined,
},
'y-axis-column-layer0X1': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)',
},
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: RECORDS_FIELD,
timeScale: undefined,
timeShift: undefined,
},
'y-axis-column-layer0X2': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'overall_sum',
params: undefined,
references: ['y-axis-column-layer0X1'],
scale: 'ratio',
},
'y-axis-column-layer0X3': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Part of Pages loaded',
operationType: 'math',
params: {
tinymathAst: {
args: ['y-axis-column-layer0X0', 'y-axis-column-layer0X2'],
location: {
max: 288,
min: 0,
},
name: 'divide',
text: "count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)') / overall_sum(count(kql='transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)'))",
type: 'function',
},
},
references: ['y-axis-column-layer0X0', 'y-axis-column-layer0X2'],
scale: 'ratio',
},
},
incompleteColumns: {},
},
'layer0-reference-lines': {
columnOrder: [
'50th-percentile-reference-line-layer0-reference-lines',
'75th-percentile-reference-line-layer0-reference-lines',
'90th-percentile-reference-line-layer0-reference-lines',
'95th-percentile-reference-line-layer0-reference-lines',
'99th-percentile-reference-line-layer0-reference-lines',
],
columns: {
'50th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '50th',
operationType: 'percentile',
params: {
percentile: 50,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'75th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '75th',
operationType: 'percentile',
params: {
percentile: 75,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'90th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '90th',
operationType: 'percentile',
params: {
percentile: 90,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'95th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '95th',
operationType: 'percentile',
params: {
percentile: 95,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
'99th-percentile-reference-line-layer0-reference-lines': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: '99th',
operationType: 'percentile',
params: {
percentile: 99,
},
scale: 'ratio',
sourceField: 'transaction.duration.us',
},
},
incompleteColumns: {},
},
},
},
},
filters: [],
query: {
language: 'kuery',
query:
'transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana) and transaction.duration.us < 60000000',
},
visualization: {
axisTitlesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
curveType: 'CURVE_MONOTONE_X',
fittingFunction: 'Linear',
gridlinesVisibilitySettings: {
x: false,
yLeft: true,
yRight: true,
},
layers: [
{
accessors: ['y-axis-column-layer0-0'],
layerId: 'layer0',
layerType: 'data',
palette: undefined,
seriesType: 'line',
xAccessor: 'x-axis-column-layer0',
yConfig: [
{
axisMode: 'left',
color: 'green',
forAccessor: 'y-axis-column-layer0-0',
},
],
},
{
accessors: [
'50th-percentile-reference-line-layer0-reference-lines',
'75th-percentile-reference-line-layer0-reference-lines',
'90th-percentile-reference-line-layer0-reference-lines',
'95th-percentile-reference-line-layer0-reference-lines',
'99th-percentile-reference-line-layer0-reference-lines',
],
layerId: 'layer0-reference-lines',
layerType: 'referenceLine',
yConfig: [
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '50th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '75th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '90th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '95th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
{
axisMode: 'bottom',
color: '#6092C0',
forAccessor: '99th-percentile-reference-line-layer0-reference-lines',
lineStyle: 'solid',
lineWidth: 2,
textVisibility: true,
},
],
},
],
legend: {
isVisible: true,
position: 'right',
showSingleSeries: true,
legendSize: 'auto',
shouldTruncate: false,
},
preferredSeriesType: 'line',
tickLabelsVisibilitySettings: {
x: true,
yLeft: true,
yRight: true,
},
valueLabels: 'hide',
},
},
title: 'Prefilled from exploratory view app',
visualizationType: 'lnsXY',
};

View file

@ -1,190 +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 { mockDataView } from '../../rtl_helpers';
export const sampleMetricFormulaAttribute = {
description: '',
references: [],
state: {
adHocDataViews: { [mockDataView.title]: mockDataView.toSpec(false) },
internalReferences: [
{
id: 'apm-*',
name: 'indexpattern-datasource-current-indexpattern',
type: 'index-pattern',
},
{
id: 'apm-*',
name: 'indexpattern-datasource-layer-layer0',
type: 'index-pattern',
},
],
datasourceStates: {
formBased: {
layers: {
layer0: {
columnOrder: [
'layer-0-column-1X0',
'layer-0-column-1X1',
'layer-0-column-1X2',
'layer-0-column-1',
],
columns: {
'layer-0-column-1': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query: 'summary.up: *',
},
isBucketed: false,
label: 'Availability',
operationType: 'formula',
params: {
format: {
id: 'percent',
params: {
decimals: 1,
},
},
formula: "1- (count(kql='summary.down > 0') / count())",
isFormulaBroken: false,
},
references: ['layer-0-column-1X2'],
},
'layer-0-column-1X0': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query: '(summary.up: *) AND (summary.down > 0)',
},
isBucketed: false,
label: 'Part of Availability',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: '___records___',
},
'layer-0-column-1X1': {
customLabel: true,
dataType: 'number',
filter: {
language: 'kuery',
query: 'summary.up: *',
},
isBucketed: false,
label: 'Part of Availability',
operationType: 'count',
params: {
emptyAsNull: false,
},
scale: 'ratio',
sourceField: '___records___',
},
'layer-0-column-1X2': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: 'Part of Availability',
operationType: 'math',
params: {
tinymathAst: {
args: [
1,
{
args: ['layer-0-column-1X0', 'layer-0-column-1X1'],
location: {
max: 44,
min: 2,
},
name: 'divide',
text: " (count(kql='summary.down > 0') / count())",
type: 'function',
},
],
location: {
max: 44,
min: 0,
},
name: 'subtract',
text: "1- (count(kql='summary.down > 0') / count())",
type: 'function',
},
},
references: ['layer-0-column-1X0', 'layer-0-column-1X1'],
scale: 'ratio',
},
},
indexPatternId: 'apm-*',
},
},
},
},
filters: [],
query: {
language: 'kuery',
query: '',
},
visualization: {
accessor: 'layer-0-column-1',
colorMode: 'Labels',
layerId: 'layer0',
layerType: 'data',
palette: {
name: 'custom',
params: {
colorStops: [
{
color: '#cc5642',
stop: 0.8,
},
{
color: '#d6bf57',
stop: 0.9,
},
{
color: '#209280',
stop: 0.95,
},
],
continuity: 'above',
maxSteps: 5,
name: 'custom',
progression: 'fixed',
rangeMax: 1,
rangeMin: 0,
rangeType: 'number',
reverse: false,
steps: 3,
stops: [
{
color: '#cc5642',
stop: 0.9,
},
{
color: '#d6bf57',
stop: 0.95,
},
{
color: '#209280',
stop: 1.9903347477604902,
},
],
},
type: 'palette',
},
size: 's',
titlePosition: 'bottom',
},
},
title: 'Prefilled from exploratory view app',
visualizationType: 'lnsLegacyMetric',
};

View file

@ -1,127 +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 rison from '@kbn/rison';
import {
buildQueryFilter,
PhraseFilter,
ExistsFilter,
buildPhraseFilter as esBuildPhraseFilter,
buildPhrasesFilter as esBuildPhrasesFilter,
buildExistsFilter as esBuildExistsFilter,
} from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/common';
import { PersistableFilter } from '@kbn/lens-plugin/common';
import type { ReportViewType, UrlFilter } from '../types';
import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage';
import { convertToShortUrl, encodeUriIfNeeded } from './exploratory_view_url';
export function createExploratoryViewRoutePath({
reportType,
allSeries,
}: {
reportType: ReportViewType;
allSeries: AllSeries;
}) {
const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series));
return `/exploratory-view/#?reportType=${reportType}&sr=${encodeUriIfNeeded(
rison.encode(allShortSeries)
)}`;
}
export function buildPhraseFilter(field: string, value: string, dataView?: DataView) {
const fieldMeta = dataView?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta && dataView) {
return [esBuildPhraseFilter(fieldMeta, value, dataView)];
}
return [];
}
export function getQueryFilter(field: string, value: string[], dataView?: DataView) {
const fieldMeta = dataView?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta && dataView) {
return value.map((val) =>
buildQueryFilter(
{
query_string: {
fields: [field],
query: `*${val}*`,
},
},
dataView.id!,
''
)
);
}
return [];
}
export function buildPhrasesFilter(
field: string,
value: Array<string | number>,
dataView?: DataView
) {
const fieldMeta = dataView?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta && dataView) {
if (value.length === 1) {
return [esBuildPhraseFilter(fieldMeta, value[0], dataView)];
}
return [esBuildPhrasesFilter(fieldMeta, value, dataView)];
}
return [];
}
export function buildExistsFilter(field: string, dataView?: DataView) {
const fieldMeta = dataView?.fields.find((fieldT) => fieldT.name === field);
if (fieldMeta && dataView) {
return [esBuildExistsFilter(fieldMeta, dataView)];
}
return [];
}
type FiltersType = Array<PersistableFilter | ExistsFilter | PhraseFilter>;
export function urlFilterToPersistedFilter({
urlFilters,
initFilters,
dataView,
}: {
urlFilters: UrlFilter[];
initFilters?: FiltersType;
dataView: DataView;
}) {
const parsedFilters: FiltersType = initFilters ? [...initFilters] : [];
urlFilters.forEach(
({ field, values = [], notValues = [], wildcards = [], notWildcards = ([] = []) }) => {
if (values.length > 0) {
const filter = buildPhrasesFilter(field, values, dataView);
parsedFilters.push(...filter);
}
if (notValues.length > 0) {
const filter = buildPhrasesFilter(field, notValues, dataView)[0];
filter.meta.negate = true;
parsedFilters.push(filter);
}
if (wildcards.length > 0) {
const filter = getQueryFilter(field, wildcards, dataView);
parsedFilters.push(...filter);
}
if (notWildcards.length > 0) {
const filter = getQueryFilter(field, notWildcards, dataView)[0];
filter.meta.negate = true;
parsedFilters.push(filter);
}
}
);
return parsedFilters;
}

View file

@ -1,74 +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 React, { createContext, useContext, useState } from 'react';
import { AppMountParameters } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import type { AppDataType, ConfigProps, ReportViewType, SeriesConfig } from '../types';
export type ReportConfigMap = Record<string, Array<(config: ConfigProps) => SeriesConfig>>;
interface ExploratoryViewContextValue {
dataTypes: Array<{ id: AppDataType; label: string }>;
reportTypes: Array<{
reportType: ReportViewType | typeof SELECT_REPORT_TYPE;
label: string;
}>;
reportConfigMap: ReportConfigMap;
asPanel?: boolean;
setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'];
theme$: AppMountParameters['theme$'];
isEditMode?: boolean;
setIsEditMode?: React.Dispatch<React.SetStateAction<boolean>>;
}
export const ExploratoryViewContext = createContext<ExploratoryViewContextValue>(
{} as ExploratoryViewContextValue
);
export function ExploratoryViewContextProvider({
children,
reportTypes,
dataTypes,
reportConfigMap,
setHeaderActionMenu,
asPanel = true,
theme$,
}: { children: JSX.Element } & ExploratoryViewContextValue) {
const [isEditMode, setIsEditMode] = useState(false);
const value = {
asPanel,
reportTypes,
dataTypes,
reportConfigMap,
setHeaderActionMenu,
theme$,
isEditMode,
setIsEditMode,
};
return (
<ExploratoryViewContext.Provider value={value}>{children}</ExploratoryViewContext.Provider>
);
}
export function useExploratoryView() {
const context = useContext(ExploratoryViewContext);
if (context === undefined) {
throw new Error('useExploratoryView must be used within a ExploratoryViewContextProvider');
}
return context;
}
export const SELECT_REPORT_TYPE = i18n.translate(
'xpack.observability.expView.seriesBuilder.selectReportType',
{
defaultMessage: 'No report type selected',
}
);

View file

@ -1,197 +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 React from 'react';
import Embeddable from './embeddable';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { DataViewState } from '../hooks/use_app_data_view';
import { render } from '../rtl_helpers';
import { AddToCaseAction } from '../header/add_to_case_action';
import { ActionTypes } from './use_actions';
jest.mock('../header/add_to_case_action', () => ({
AddToCaseAction: jest.fn(() => <div>mockAddToCaseAction</div>),
}));
const mockLensAttrs = {
hidePanelTitles: true,
description: '',
visualizationType: 'lnsMetric',
state: {
visualization: {
accessor: 'b00c65ea-32be-4163-bfc8-f795b1ef9d06',
layerId: '416b6fad-1923-4f6a-a2df-b223bb287e30',
layerType: 'data',
},
query: {
language: 'kuery',
query: '',
},
filters: [],
datasourceStates: {
formBased: {
layers: {
'416b6fad-1923-4f6a-a2df-b223bb287e30': {
columnOrder: ['b00c65ea-32be-4163-bfc8-f795b1ef9d06'],
columns: {
'b00c65ea-32be-4163-bfc8-f795b1ef9d06': {
customLabel: true,
dataType: 'number',
isBucketed: false,
label: ' ',
operationType: 'unique_count',
scale: 'ratio',
sourceField: 'host.name',
},
},
incompleteColumns: {},
},
},
},
},
},
references: [
{
type: 'index-pattern',
id: 'security-solution-default',
name: 'indexpattern-datasource-current-indexpattern',
},
{
type: 'index-pattern',
id: 'security-solution-default',
name: 'indexpattern-datasource-layer-416b6fad-1923-4f6a-a2df-b223bb287e30',
},
{
type: 'tag',
id: 'security-solution-default',
name: 'tag-ref-security-solution-default',
},
],
};
const mockTimeRange = {
from: '2022-02-15T16:00:00.000Z',
to: '2022-02-16T15:59:59.999Z',
};
const mockOwner = 'securitySolution';
const mockAppId = 'securitySolutionUI';
const mockDataViews = {} as DataViewState;
const mockReportType = 'kpi-over-time';
const mockTitle = 'mockTitle';
const mockLens = {
EmbeddableComponent: jest.fn((props) => {
return (
<div
data-test-subj={
props.id === 'exploratoryView-singleMetric'
? 'exploratoryView-singleMetric'
: 'exploratoryView'
}
>
mockEmbeddableComponent
</div>
);
}),
SaveModalComponent: jest.fn(() => <div>mockSaveModalComponent</div>),
} as unknown as LensPublicStart;
const mockActions: ActionTypes[] = ['addToCase', 'openInLens'];
describe('Embeddable', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders title', async () => {
const { container, getByText } = render(
<Embeddable
appId={mockAppId}
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
dataViewState={mockDataViews}
lens={mockLens}
reportType={mockReportType}
title={mockTitle}
withActions={mockActions}
/>
);
expect(container.querySelector(`[data-test-subj="exploratoryView-title"]`)).toBeInTheDocument();
expect(getByText(mockTitle)).toBeInTheDocument();
});
it('renders no title if it is not given', async () => {
const { container } = render(
<Embeddable
appId={mockAppId}
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
dataViewState={mockDataViews}
lens={mockLens}
reportType={mockReportType}
withActions={mockActions}
/>
);
expect(
container.querySelector(`[data-test-subj="exploratoryView-title"]`)
).not.toBeInTheDocument();
});
it('renders lens component', () => {
const { container } = render(
<Embeddable
appId={mockAppId}
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
dataViewState={mockDataViews}
lens={mockLens}
reportType={mockReportType}
withActions={mockActions}
/>
);
expect(
container.querySelector(`[data-test-subj="exploratoryView-singleMetric"]`)
).not.toBeInTheDocument();
expect(container.querySelector(`[data-test-subj="exploratoryView"]`)).toBeInTheDocument();
expect((mockLens.EmbeddableComponent as jest.Mock).mock.calls[0][0].id).toEqual(
'exploratoryView'
);
expect((mockLens.EmbeddableComponent as jest.Mock).mock.calls[0][0].attributes).toEqual(
mockLensAttrs
);
expect((mockLens.EmbeddableComponent as jest.Mock).mock.calls[0][0].timeRange).toEqual(
mockTimeRange
);
expect((mockLens.EmbeddableComponent as jest.Mock).mock.calls[0][0].timeRange).toEqual(
mockTimeRange
);
expect((mockLens.EmbeddableComponent as jest.Mock).mock.calls[0][0].withDefaultActions).toEqual(
true
);
});
it('renders AddToCaseAction', () => {
render(
<Embeddable
appId={mockAppId}
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
dataViewState={mockDataViews}
isSingleMetric={true}
lens={mockLens}
reportType={mockReportType}
withActions={mockActions}
/>
);
expect((AddToCaseAction as jest.Mock).mock.calls[0][0].timeRange).toEqual(mockTimeRange);
expect((AddToCaseAction as jest.Mock).mock.calls[0][0].appId).toEqual(mockAppId);
expect((AddToCaseAction as jest.Mock).mock.calls[0][0].lensAttributes).toEqual(mockLensAttrs);
expect((AddToCaseAction as jest.Mock).mock.calls[0][0].owner).toEqual(mockOwner);
});
});

View file

@ -1,317 +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 { Position } from '@elastic/charts';
import React, { useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui';
import {
FormulaPublicApi,
LensEmbeddableInput,
LensPublicStart,
XYState,
} from '@kbn/lens-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/common';
import styled from 'styled-components';
import { useKibanaSpace } from '../../../../hooks/use_kibana_space';
import { HeatMapLensAttributes } from '../configurations/lens_attributes/heatmap_attributes';
import { SingleMetricLensAttributes } from '../configurations/lens_attributes/single_metric_attributes';
import { AllSeries, ReportTypes, useTheme } from '../../../..';
import { LayerConfig, LensAttributes } from '../configurations/lens_attributes';
import { AppDataType, ReportViewType } from '../types';
import { getLayerConfigs } from '../hooks/use_lens_attributes';
import { OperationTypeComponent } from '../series_editor/columns/operation_type_select';
import { DataViewState } from '../hooks/use_app_data_view';
import { ReportConfigMap } from '../contexts/exploratory_view_config';
import { obsvReportConfigMap } from '../obsv_exploratory_view';
import { ActionTypes, useActions } from './use_actions';
import { AddToCaseAction } from '../header/add_to_case_action';
import { observabilityFeatureId } from '../../../../../common';
export interface ExploratoryEmbeddableProps {
id?: string;
appId?: 'securitySolutionUI' | 'observability';
appendTitle?: JSX.Element;
attributes?: AllSeries;
axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings'];
gridlinesVisibilitySettings?: XYState['gridlinesVisibilitySettings'];
customHeight?: string;
customLensAttrs?: any; // Takes LensAttributes
customTimeRange?: { from: string; to: string }; // required if rendered with LensAttributes
dataTypesIndexPatterns?: Partial<Record<AppDataType, string>>;
isSingleMetric?: boolean;
legendIsVisible?: boolean;
legendPosition?: Position;
hideTicks?: boolean;
onBrushEnd?: (param: { range: number[] }) => void;
onLoad?: (loading: boolean) => void;
caseOwner?: string;
reportConfigMap?: ReportConfigMap;
reportType: ReportViewType;
showCalculationMethod?: boolean;
title?: string | JSX.Element;
withActions?: boolean | ActionTypes[];
align?: 'left' | 'right' | 'center';
sparklineMode?: boolean;
noLabel?: boolean;
fontSize?: number;
lineHeight?: number;
dataTestSubj?: string;
searchSessionId?: string;
}
export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps {
lens: LensPublicStart;
dataViewState: DataViewState;
lensFormulaHelper?: FormulaPublicApi;
}
// eslint-disable-next-line import/no-default-export
export default function Embeddable({
appId,
appendTitle,
attributes = [],
axisTitlesVisibility,
gridlinesVisibilitySettings,
customHeight,
customLensAttrs,
customTimeRange,
dataViewState,
legendIsVisible,
legendPosition,
lens,
onBrushEnd,
caseOwner = observabilityFeatureId,
reportConfigMap = {},
reportType,
showCalculationMethod = false,
title,
withActions = true,
lensFormulaHelper,
hideTicks,
align,
noLabel,
fontSize = 27,
lineHeight = 32,
searchSessionId,
onLoad,
}: ExploratoryEmbeddableComponentProps) {
const LensComponent = lens?.EmbeddableComponent;
const LensSaveModalComponent = lens?.SaveModalComponent;
const [isSaveOpen, setIsSaveOpen] = useState(false);
const [isAddToCaseOpen, setAddToCaseOpen] = useState(false);
const spaceId = useKibanaSpace();
const series = Object.entries(attributes)[0]?.[1];
const [operationType, setOperationType] = useState(series?.operationType);
const theme = useTheme();
const layerConfigs: LayerConfig[] = getLayerConfigs(
attributes,
reportType,
theme,
dataViewState,
{ ...reportConfigMap, ...obsvReportConfigMap },
spaceId.space?.id
);
let lensAttributes;
let attributesJSON = customLensAttrs;
if (!customLensAttrs) {
try {
if (reportType === ReportTypes.SINGLE_METRIC) {
lensAttributes = new SingleMetricLensAttributes(
layerConfigs,
reportType,
lensFormulaHelper!
);
attributesJSON = lensAttributes?.getJSON('lnsLegacyMetric');
} else if (reportType === ReportTypes.HEATMAP) {
lensAttributes = new HeatMapLensAttributes(layerConfigs, reportType, lensFormulaHelper!);
attributesJSON = lensAttributes?.getJSON('lnsHeatmap');
} else {
lensAttributes = new LensAttributes(layerConfigs, reportType, lensFormulaHelper);
attributesJSON = lensAttributes?.getJSON();
}
// eslint-disable-next-line no-empty
} catch (error) {}
}
const timeRange = customTimeRange ?? series?.time;
const actions = useActions({
withActions,
attributes,
reportType,
appId,
setIsSaveOpen,
setAddToCaseOpen,
lensAttributes: attributesJSON,
timeRange,
});
if (!attributesJSON) {
return null;
}
if (typeof axisTitlesVisibility !== 'undefined') {
(attributesJSON.state.visualization as XYState).axisTitlesVisibilitySettings =
axisTitlesVisibility;
}
if (typeof gridlinesVisibilitySettings !== 'undefined') {
(attributesJSON.state.visualization as XYState).gridlinesVisibilitySettings =
gridlinesVisibilitySettings;
}
if (typeof legendIsVisible !== 'undefined') {
(attributesJSON.state.visualization as XYState).legend.isVisible = legendIsVisible;
}
if (typeof legendPosition !== 'undefined') {
(attributesJSON.state.visualization as XYState).legend.position = legendPosition;
}
if (hideTicks) {
(attributesJSON.state.visualization as XYState).tickLabelsVisibilitySettings = {
x: false,
yRight: false,
yLeft: false,
};
}
if (!attributesJSON && layerConfigs.length < 1) {
return null;
}
if (!LensComponent) {
return <EuiText>No lens component</EuiText>;
}
attributesJSON.state.searchSessionId = searchSessionId;
attributesJSON.searchSessionId = searchSessionId;
return (
<Wrapper
$customHeight={customHeight}
align={align}
noLabel={noLabel}
fontSize={fontSize}
lineHeight={lineHeight}
>
{(title || showCalculationMethod || appendTitle) && (
<EuiFlexGroup alignItems="center" gutterSize="none">
{title && (
<EuiFlexItem data-test-subj="exploratoryView-title">
<EuiTitle size="xs">
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
)}
{showCalculationMethod && (
<EuiFlexItem grow={false} style={{ minWidth: 150 }}>
<OperationTypeComponent
operationType={operationType}
onChange={(val) => {
setOperationType(val);
}}
/>
</EuiFlexItem>
)}
{appendTitle && appendTitle}
</EuiFlexGroup>
)}
<LensComponent
id="exploratoryView"
data-test-subj="exploratoryView"
style={{ height: '100%' }}
timeRange={timeRange}
attributes={{ ...attributesJSON, title: undefined, hidePanelTitles: true, description: '' }}
onBrushEnd={onBrushEnd}
withDefaultActions={Boolean(withActions)}
extraActions={actions}
viewMode={ViewMode.VIEW}
searchSessionId={searchSessionId}
onLoad={onLoad}
/>
{isSaveOpen && attributesJSON && (
<LensSaveModalComponent
initialInput={attributesJSON as unknown as LensEmbeddableInput}
onClose={() => setIsSaveOpen(false)}
// if we want to do anything after the viz is saved
// right now there is no action, so an empty function
onSave={() => {}}
/>
)}
<AddToCaseAction
lensAttributes={attributesJSON}
timeRange={customTimeRange ?? series?.time}
autoOpen={isAddToCaseOpen}
setAutoOpen={setAddToCaseOpen}
appId={appId}
owner={caseOwner}
/>
</Wrapper>
);
}
const Wrapper = styled.div<{
$customHeight?: string | number;
align?: 'left' | 'right' | 'center';
noLabel?: boolean;
fontSize?: number;
lineHeight?: number;
}>`
height: ${(props) => (props.$customHeight ? `${props.$customHeight};` : `100%;`)};
position: relative;
&&& {
> :nth-child(2) {
height: ${(props) =>
props.$customHeight ? `${props.$customHeight};` : `calc(100% - 32px);`};
}
.expExpressionRenderer__expression {
padding: 0 !important;
}
.legacyMtrVis {
> :first-child {
justify-content: ${(props) =>
props.align === 'left'
? `flex-start;`
: props.align === 'right'
? `flex-end;`
: 'center;'};
}
justify-content: flex-end;
.legacyMtrVis__container {
padding: 0;
> :nth-child(2) {
${({ noLabel }) =>
noLabel &&
` display: none;
`}
}
}
.legacyMtrVis__value {
line-height: ${({ lineHeight }) => lineHeight}px !important;
font-size: ${({ fontSize }) => fontSize}px !important;
}
> :first-child {
transform: none !important;
}
}
.euiLoadingChart {
position: absolute;
top: 50%;
right: 50%;
transform: translate(50%, -50%);
}
}
`;

View file

@ -1,179 +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 React, { useCallback, useMemo, useState, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import type { CoreStart } from '@kbn/core/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { EuiErrorBoundary } from '@elastic/eui';
import styled from 'styled-components';
import { DataView } from '@kbn/data-views-plugin/common';
import { FormulaPublicApi } from '@kbn/lens-plugin/public';
import { i18n } from '@kbn/i18n';
import { useAppDataView } from './use_app_data_view';
import { ObservabilityPublicPluginsStart, useFetcher } from '../../../..';
import type { ExploratoryEmbeddableProps, ExploratoryEmbeddableComponentProps } from './embeddable';
const Embeddable = React.lazy(() => import('./embeddable'));
function ExploratoryViewEmbeddable(props: ExploratoryEmbeddableComponentProps) {
return (
<React.Suspense fallback={<EuiLoadingSpinner />}>
<Embeddable {...props} />
</React.Suspense>
);
}
export function getExploratoryViewEmbeddable(
services: CoreStart & ObservabilityPublicPluginsStart
) {
const { lens, dataViews: dataViewsService, uiSettings } = services;
const dataViewCache: Record<string, DataView> = {};
const lenStateHelperPromise: Promise<{ formula: FormulaPublicApi }> | null = null;
const lastRefreshed: Record<string, { from: string; to: string }> = {};
const hasSameTimeRange = (props: ExploratoryEmbeddableProps) => {
const { attributes } = props;
if (!attributes || attributes?.length === 0) {
return false;
}
const series = attributes[0];
const { time } = series;
const { from, to } = time;
return attributes.every((seriesT) => {
const { time: timeT } = seriesT;
return timeT.from === from && timeT.to === to;
});
};
return (props: ExploratoryEmbeddableProps) => {
useEffect(() => {
if (!services.data.search.session.getSessionId()) {
services.data.search.session.start();
}
}, []);
const { dataTypesIndexPatterns, attributes, customHeight } = props;
if (!dataViewsService || !lens || !attributes || attributes?.length === 0) {
return null;
}
const series = attributes[0];
const isDarkMode = uiSettings?.get('theme:darkMode');
const { data: lensHelper, loading: lensLoading } = useFetcher(async () => {
if (lenStateHelperPromise) {
return lenStateHelperPromise;
}
return lens.stateHelperApi();
}, []);
const [loadCount, setLoadCount] = useState(0);
const onLensLoaded = useCallback(
(lensLoaded: boolean) => {
if (lensLoaded && props.id && hasSameTimeRange(props) && !lastRefreshed[props.id]) {
lastRefreshed[props.id] = series.time;
}
setLoadCount((prev) => prev + 1);
},
[props, series.time]
);
const { dataViews, loading } = useAppDataView({
series,
dataViewCache,
dataViewsService,
dataTypesIndexPatterns,
seriesDataType: series?.dataType,
});
const embedProps = useMemo(() => {
const newProps = { ...props };
if (props.sparklineMode) {
newProps.axisTitlesVisibility = { x: false, yRight: false, yLeft: false };
newProps.legendIsVisible = false;
newProps.hideTicks = true;
}
if (props.id && lastRefreshed[props.id] && loadCount < 2) {
newProps.attributes = props.attributes?.map((seriesT) => ({
...seriesT,
time: lastRefreshed[props.id!],
}));
} else if (props.id) {
lastRefreshed[props.id] = series.time;
}
return newProps;
}, [loadCount, props, series.time]);
if (Object.keys(dataViews).length === 0 || loading || !lensHelper || lensLoading) {
return (
<LoadingWrapper customHeight={customHeight}>
<EuiLoadingSpinner size="l" />
</LoadingWrapper>
);
}
if (!dataViews[series?.dataType]) {
return <EmptyState height={props.customHeight} />;
}
return (
<EuiErrorBoundary>
<EuiThemeProvider darkMode={isDarkMode}>
<KibanaContextProvider services={services}>
<Wrapper customHeight={props.customHeight} data-test-subj={props.dataTestSubj}>
<ExploratoryViewEmbeddable
{...embedProps}
dataViewState={dataViews}
lens={lens}
lensFormulaHelper={lensHelper.formula}
searchSessionId={services.data.search.session.getSessionId()}
onLoad={onLensLoaded}
/>
</Wrapper>
</KibanaContextProvider>
</EuiThemeProvider>
</EuiErrorBoundary>
);
};
}
const Wrapper = styled.div<{
customHeight?: string;
}>`
height: ${(props) => (props.customHeight ? `${props.customHeight};` : `100%;`)};
`;
const LoadingWrapper = styled.div<{
customHeight?: string;
}>`
height: ${(props) => (props.customHeight ? `${props.customHeight};` : `100%;`)};
display: flex;
align-items: center;
justify-content: center;
`;
function EmptyState({ height }: { height?: string }) {
return (
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: height ?? '100%' }}>
<EuiFlexItem grow={false}>
<span>{NO_DATA_LABEL}</span>
</EuiFlexItem>
</EuiFlexGroup>
);
}
const NO_DATA_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.noData', {
defaultMessage: 'No data',
});

View file

@ -1,197 +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 { useCallback, useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Action, ActionExecutionContext } from '@kbn/ui-actions-plugin/public';
import { createExploratoryViewRoutePath } from '../configurations/utils';
import { createExploratoryViewUrl } from '../configurations/exploratory_view_url';
import { ReportViewType } from '../types';
import { AllSeries } from '../hooks/use_series_storage';
import { ObservabilityAppServices } from '../../../../application/types';
export type ActionTypes = 'explore' | 'save' | 'addToCase' | 'openInLens';
export function useActions({
withActions,
attributes,
reportType,
setIsSaveOpen,
setAddToCaseOpen,
appId = 'observability',
timeRange,
lensAttributes,
}: {
withActions?: boolean | ActionTypes[];
reportType: ReportViewType;
attributes: AllSeries;
appId?: 'securitySolutionUI' | 'observability';
setIsSaveOpen: (val: boolean) => void;
setAddToCaseOpen: (val: boolean) => void;
timeRange: { from: string; to: string };
lensAttributes: any;
}) {
const kServices = useKibana<ObservabilityAppServices>().services;
const { lens } = kServices;
const [defaultActions, setDefaultActions] = useState(['explore', 'save', 'addToCase']);
useEffect(() => {
if (withActions === false) {
setDefaultActions([]);
}
if (Array.isArray(withActions)) {
setDefaultActions(withActions);
}
}, [withActions]);
const { http, application } = useKibana().services;
const href = createExploratoryViewUrl(
{ reportType, allSeries: attributes },
http?.basePath.get(),
appId
);
const routePath = createExploratoryViewRoutePath({ reportType, allSeries: attributes });
const openInLensCallback = useCallback(() => {
if (lensAttributes) {
lens.navigateToPrefilledEditor(
{
id: '',
timeRange,
attributes: lensAttributes,
},
{
openInNewTab: true,
}
);
}
}, [lens, lensAttributes, timeRange]);
const exploreCallback = useCallback(() => {
application?.navigateToApp(appId, { path: routePath });
}, [appId, application, routePath]);
const saveCallback = useCallback(() => {
setIsSaveOpen(true);
}, [setIsSaveOpen]);
const addToCaseCallback = useCallback(() => {
setAddToCaseOpen(true);
}, [setAddToCaseOpen]);
return defaultActions.map<Action>((action) => {
if (action === 'save') {
return getSaveAction({ callback: saveCallback });
}
if (action === 'addToCase') {
return getAddToCaseAction({ callback: addToCaseCallback });
}
if (action === 'openInLens') {
return getOpenInLensAction({ callback: openInLensCallback });
}
return getExploreAction({ href, callback: exploreCallback });
});
}
const getOpenInLensAction = ({ callback }: { callback: () => void }): Action => {
return {
id: 'expViewOpenInLens',
getDisplayName(context: ActionExecutionContext<object>): string {
return i18n.translate('xpack.observability.expView.openInLens', {
defaultMessage: 'Open in Lens',
});
},
getIconType(context: ActionExecutionContext<object>): string | undefined {
return 'visArea';
},
type: 'link',
async isCompatible(context: ActionExecutionContext<object>): Promise<boolean> {
return true;
},
async execute(context: ActionExecutionContext<object>): Promise<void> {
callback();
return;
},
};
};
const getExploreAction = ({ href, callback }: { href: string; callback: () => void }): Action => {
return {
id: 'expViewExplore',
getDisplayName(context: ActionExecutionContext<object>): string {
return i18n.translate('xpack.observability.expView.explore', {
defaultMessage: 'Explore',
});
},
getIconType(context: ActionExecutionContext<object>): string | undefined {
return 'visArea';
},
type: 'link',
async isCompatible(context: ActionExecutionContext<object>): Promise<boolean> {
return true;
},
async getHref(context: ActionExecutionContext<object>): Promise<string | undefined> {
return href;
},
async execute(context: ActionExecutionContext<object>): Promise<void> {
callback();
return;
},
order: 50,
};
};
const getSaveAction = ({ callback }: { callback: () => void }): Action => {
return {
id: 'expViewSave',
getDisplayName(context: ActionExecutionContext<object>): string {
return i18n.translate('xpack.observability.expView.save', {
defaultMessage: 'Save visualization',
});
},
getIconType(context: ActionExecutionContext<object>): string | undefined {
return 'save';
},
type: 'actionButton',
async isCompatible(context: ActionExecutionContext<object>): Promise<boolean> {
return true;
},
async execute(context: ActionExecutionContext<object>): Promise<void> {
callback();
return;
},
order: 49,
};
};
const getAddToCaseAction = ({ callback }: { callback: () => void }): Action => {
return {
id: 'expViewAddToCase',
getDisplayName(context: ActionExecutionContext<object>): string {
return i18n.translate('xpack.observability.expView.addToCase', {
defaultMessage: 'Add to case',
});
},
getIconType(context: ActionExecutionContext<object>): string | undefined {
return 'link';
},
type: 'actionButton',
async isCompatible(context: ActionExecutionContext<object>): Promise<boolean> {
return true;
},
async execute(context: ActionExecutionContext<object>): Promise<void> {
callback();
return;
},
order: 48,
};
};

View file

@ -1,55 +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 { useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import { useLocalDataView } from './use_local_data_view';
import {
ExploratoryEmbeddableProps,
ObservabilityPublicPluginsStart,
useFetcher,
} from '../../../..';
import type { DataViewState } from '../hooks/use_app_data_view';
import type { AppDataType } from '../types';
import { ObservabilityDataViews } from '../../../../utils/observability_data_views/observability_data_views';
import { SeriesUrl } from '../../../..';
export const useAppDataView = ({
series,
dataViewCache,
seriesDataType,
dataViewsService,
dataTypesIndexPatterns,
}: {
series: SeriesUrl;
seriesDataType: AppDataType;
dataViewCache: Record<string, DataView>;
dataViewsService: ObservabilityPublicPluginsStart['dataViews'];
dataTypesIndexPatterns: ExploratoryEmbeddableProps['dataTypesIndexPatterns'];
}) => {
const [dataViews, setDataViews] = useState<DataViewState>({} as DataViewState);
const { dataViewTitle } = useLocalDataView(seriesDataType, dataTypesIndexPatterns);
const { loading } = useFetcher(async () => {
if (dataViewTitle && !dataViews[seriesDataType]) {
if (dataViewCache[dataViewTitle]) {
setDataViews((prevState) => ({
...(prevState ?? {}),
[seriesDataType]: dataViewCache[dataViewTitle],
}));
} else {
const obsvIndexP = new ObservabilityDataViews(dataViewsService, true);
const indPattern = await obsvIndexP.getDataView(seriesDataType, dataViewTitle);
dataViewCache[dataViewTitle] = indPattern!;
setDataViews((prevState) => ({ ...(prevState ?? {}), [seriesDataType]: indPattern }));
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataViewTitle, seriesDataType, JSON.stringify(series)]);
return { dataViews, loading };
};

View file

@ -1,39 +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 { useEffect } from 'react';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { getDataTypeIndices } from '../../../../utils/observability_data_views';
import { AppDataType } from '../types';
import { ExploratoryEmbeddableProps, useFetcher } from '../../../..';
export function useLocalDataView(
seriesDataType: AppDataType,
dataTypesIndexPatterns: ExploratoryEmbeddableProps['dataTypesIndexPatterns']
) {
const [dataViewTitle, setDataViewTitle] = useLocalStorage(
`${seriesDataType}AppDataViewTitle`,
''
);
const initDatViewTitle = dataTypesIndexPatterns?.[seriesDataType];
const { data: updatedDataViewTitle } = useFetcher(async () => {
if (initDatViewTitle) {
return initDatViewTitle;
}
return (await getDataTypeIndices(seriesDataType)).indices;
}, [initDatViewTitle, seriesDataType]);
useEffect(() => {
if (updatedDataViewTitle) {
setDataViewTitle(updatedDataViewTitle);
}
}, [setDataViewTitle, updatedDataViewTitle]);
return { dataViewTitle: dataViewTitle || initDatViewTitle };
}

View file

@ -1,81 +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 React from 'react';
import { screen } from '@testing-library/dom';
import { render, mockAppDataView } from './rtl_helpers';
import { ExploratoryView } from './exploratory_view';
import * as obsvDataViews from '../../../utils/observability_data_views/observability_data_views';
import * as pluginHook from '../../../hooks/use_plugin_context';
import { createStubIndexPattern } from '@kbn/data-plugin/common/stubs';
import { noCasesPermissions as mockUseGetCasesPermissions } from '../../../utils/cases_permissions';
jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({
appMountParameters: {
setHeaderActionMenu: jest.fn(),
},
} as any);
jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
}));
describe('ExploratoryView', () => {
mockAppDataView();
beforeEach(() => {
const indexPattern = createStubIndexPattern({
spec: {
id: 'apm-*',
title: 'apm-*',
timeFieldName: '@timestamp',
fields: {
'@timestamp': {
name: '@timestamp',
type: 'date',
esTypes: ['date'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
},
},
});
jest.spyOn(obsvDataViews, 'ObservabilityDataViews').mockReturnValue({
getDataView: jest.fn().mockReturnValue(indexPattern),
} as any);
});
it('renders exploratory view', async () => {
render(<ExploratoryView />, { initSeries: { data: [] } });
expect(await screen.findByText(/No series found. Please add a series./i)).toBeInTheDocument();
expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument();
});
it('renders lens component when there is series', async () => {
render(<ExploratoryView />);
expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument();
expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument();
expect(screen.getByTestId('exploratoryViewSeriesPanel0')).toBeInTheDocument();
});
it('shows/hides the chart', async () => {
render(<ExploratoryView />);
const toggleButton = await screen.findByText('Hide chart');
expect(toggleButton).toBeInTheDocument();
toggleButton.click();
expect(toggleButton.textContent).toBe('Show chart');
expect(screen.queryByText('Refresh')).toBeNull();
});
});

View file

@ -1,225 +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 React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import {
EuiButtonEmpty,
EuiResizableContainer,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { ObservabilityPublicPluginsStart } from '../../../plugin';
import { useSeriesStorage } from './hooks/use_series_storage';
import { useLensAttributes } from './hooks/use_lens_attributes';
import { useAppDataViewContext } from './hooks/use_app_data_view';
import { SeriesViews } from './views/series_views';
import { LensEmbeddable } from './lens_embeddable';
import { EmptyView } from './components/empty_view';
import { useExpViewTimeRange } from './hooks/use_time_range';
import { ExpViewActionMenu } from './components/action_menu';
import { useExploratoryView } from './contexts/exploratory_view_config';
export type PanelId = 'seriesPanel' | 'chartPanel';
export function ExploratoryView({
saveAttributes,
}: {
saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void;
}) {
const {
services: { lens },
} = useKibana<ObservabilityPublicPluginsStart>();
const seriesBuilderRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<string>('100vh');
const { isEditMode } = useExploratoryView();
const [lensAttributes, setLensAttributes] = useState<TypedLensByValueInput['attributes'] | null>(
null
);
const { loadDataView, loading } = useAppDataViewContext();
const { firstSeries, allSeries, lastRefresh, reportType, setChartTimeRangeContext } =
useSeriesStorage();
const lensAttributesT = useLensAttributes();
const timeRange = useExpViewTimeRange();
const setHeightOffset = () => {
if (seriesBuilderRef?.current && wrapperRef.current) {
const headerOffset = wrapperRef.current.getBoundingClientRect().top;
setHeight(`calc(100vh - ${headerOffset + 40}px)`);
}
};
useEffect(() => {
allSeries.forEach((seriesT) => {
loadDataView({
dataType: seriesT.dataType,
});
});
}, [allSeries, loadDataView]);
useEffect(() => {
setLensAttributes(lensAttributesT);
if (saveAttributes) {
saveAttributes(lensAttributesT);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]);
useEffect(() => {
setHeightOffset();
});
const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>();
const [hiddenPanel, setHiddenPanel] = useState('');
const onCollapse = (panelId: string) => {
setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId));
};
const onChange = (panelId: PanelId) => {
onCollapse(panelId);
if (collapseFn.current) {
collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left');
}
};
return lens ? (
<>
<ExpViewActionMenu timeRange={timeRange} lensAttributes={lensAttributes} />
<LensWrapper ref={wrapperRef} height={height}>
<ResizableContainer direction="vertical" onToggleCollapsed={onCollapse}>
{(EuiResizablePanel, _EuiResizableButton, { togglePanel }) => {
collapseFn.current = (id, direction) => togglePanel?.(id, { direction });
return (
<>
<EuiFlexGroup alignItems="center" gutterSize="none">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="o11yExploratoryViewButton"
size="xs"
{...(hiddenPanel === 'chartPanel'
? { iconType: 'arrowRight' }
: { iconType: 'arrowDown' })}
onClick={() => onChange('chartPanel')}
>
{hiddenPanel === 'chartPanel' ? SHOW_CHART_LABEL : HIDE_CHART_LABEL}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
<EuiResizablePanel
initialSize={isEditMode ? 40 : 60}
minSize={'30%'}
mode={'collapsible'}
id="chartPanel"
paddingSize="s"
>
{lensAttributes ? (
<LensEmbeddable
setChartTimeRangeContext={setChartTimeRangeContext}
lensAttributes={lensAttributes}
/>
) : (
<EmptyView series={firstSeries} loading={loading} reportType={reportType} />
)}
</EuiResizablePanel>
<EuiResizablePanel
initialSize={isEditMode ? 60 : 40}
minSize="10%"
mode={'main'}
id="seriesPanel"
color="subdued"
className="paddingTopSmall"
>
<SeriesViews
seriesBuilderRef={seriesBuilderRef}
onSeriesPanelCollapse={onChange}
/>
</EuiResizablePanel>
</>
);
}}
</ResizableContainer>
{hiddenPanel === 'seriesPanel' && (
<ShowPreview onClick={() => onChange('seriesPanel')} iconType="arrowUp">
{PREVIEW_LABEL}
</ShowPreview>
)}
</LensWrapper>
</>
) : (
<EuiTitle>
<h2>{LENS_NOT_AVAILABLE}</h2>
</EuiTitle>
);
}
const LensWrapper = styled.div<{ height: string }>`
min-height: 400px;
height: ${(props) => props.height};
&&& > div {
height: 100%;
}
`;
const ResizableContainer = styled(EuiResizableContainer)`
height: 100%;
&&& .paddingTopSmall {
padding-top: 8px;
}
#chartPanel {
> .euiPanel {
padding-bottom: 0;
padding-top: 0;
}
.expExpressionRenderer__expression {
padding-bottom: 0 !important;
padding-top: 0 !important;
}
}
`;
const ShowPreview = styled(EuiButtonEmpty)`
position: absolute;
bottom: 34px;
`;
const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', {
defaultMessage: 'Preview',
});
const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', {
defaultMessage: 'Hide chart',
});
const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', {
defaultMessage: 'Show chart',
});
const LENS_NOT_AVAILABLE = i18n.translate(
'xpack.observability.overview.exploratoryView.lensDisabled',
{
defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.',
}
);

View file

@ -1,125 +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 React from 'react';
import { render, forNearestButton } from '../rtl_helpers';
import { fireEvent } from '@testing-library/dom';
import { AddToCaseAction } from './add_to_case_action';
import * as useCaseHook from '../hooks/use_add_to_case';
import * as datePicker from '../components/date_range_picker';
import moment from 'moment';
import { noCasesPermissions as mockUseGetCasesPermissions } from '../../../../utils/cases_permissions';
jest.mock('../../../../hooks/use_get_user_cases_permissions', () => ({
useGetUserCasesPermissions: jest.fn(() => mockUseGetCasesPermissions()),
}));
describe('AddToCaseAction', function () {
beforeEach(() => {
jest.spyOn(datePicker, 'parseRelativeDate').mockRestore();
});
it('should render properly', async function () {
const { findByText } = render(
<AddToCaseAction
lensAttributes={{ title: 'Performance distribution' } as any}
timeRange={{ to: 'now', from: 'now-5m' }}
/>
);
expect(await findByText('Add to case')).toBeInTheDocument();
});
it('should parse relative data to the useAddToCase hook', async function () {
const useAddToCaseHook = jest.spyOn(useCaseHook, 'useAddToCase');
jest.spyOn(datePicker, 'parseRelativeDate').mockReturnValue(moment('2021-11-10T10:52:06.091Z'));
const { findByText } = render(
<AddToCaseAction
lensAttributes={{ title: 'Performance distribution' } as any}
timeRange={{ to: 'now', from: 'now-5m' }}
/>
);
expect(await findByText('Add to case')).toBeInTheDocument();
expect(useAddToCaseHook).toHaveBeenCalledWith(
expect.objectContaining({
lensAttributes: {
title: 'Performance distribution',
},
timeRange: {
from: '2021-11-10T10:52:06.091Z',
to: '2021-11-10T10:52:06.091Z',
},
})
);
});
it('should use an empty time-range when timeRanges are empty', async function () {
const useAddToCaseHook = jest.spyOn(useCaseHook, 'useAddToCase');
const { getByText } = render(
<AddToCaseAction
lensAttributes={null}
timeRange={{ to: '', from: '' }}
appId="securitySolutionUI"
owner="security"
/>
);
expect(await forNearestButton(getByText)('Add to case')).toBeDisabled();
expect(useAddToCaseHook).toHaveBeenCalledWith(
expect.objectContaining({
lensAttributes: null,
timeRange: {
from: '',
to: '',
},
appId: 'securitySolutionUI',
owner: 'security',
})
);
});
it('should be able to click add to case button', async function () {
const initSeries = {
data: [
{
name: 'test-series',
dataType: 'synthetics' as const,
reportType: 'kpi-over-time' as const,
breakdown: 'monitor.status',
time: { from: 'now-15m', to: 'now' },
},
],
};
const { findByText, core } = render(
<AddToCaseAction
lensAttributes={{ title: 'Performance distribution' } as any}
timeRange={{ to: 'now', from: 'now-5m' }}
/>,
{ initSeries }
);
fireEvent.click(await findByText('Add to case'));
expect(core?.cases?.ui.getAllCasesSelectorModal).toHaveBeenCalledTimes(1);
expect(core?.cases?.ui.getAllCasesSelectorModal).toHaveBeenCalledWith(
expect.objectContaining({
owner: ['observability'],
permissions: {
all: false,
create: false,
read: false,
update: false,
delete: false,
push: false,
},
})
);
});
});

View file

@ -1,137 +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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useEffect } from 'react';
import { toMountPoint, useKibana } from '@kbn/kibana-react-plugin/public';
import {
CasesDeepLinkId,
generateCaseViewPath,
GetAllCasesSelectorModalProps,
} from '@kbn/cases-plugin/public';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions';
import { ObservabilityAppServices } from '../../../../application/types';
import { useAddToCase } from '../hooks/use_add_to_case';
import { observabilityFeatureId, observabilityAppId } from '../../../../../common';
import { parseRelativeDate } from '../components/date_range_picker';
export interface AddToCaseProps {
appId?: 'securitySolutionUI' | 'observability';
autoOpen?: boolean;
lensAttributes: TypedLensByValueInput['attributes'] | null;
owner?: string;
setAutoOpen?: (val: boolean) => void;
timeRange: { from: string; to: string };
}
export function AddToCaseAction({
appId,
autoOpen,
lensAttributes,
owner = observabilityFeatureId,
setAutoOpen,
timeRange,
}: AddToCaseProps) {
const kServices = useKibana<ObservabilityAppServices>().services;
const userCasesPermissions = useGetUserCasesPermissions();
const {
cases,
application: { getUrlForApp },
theme,
} = kServices;
const getToastText = useCallback(
(theCase) =>
toMountPoint(
<CaseToastText
linkUrl={getUrlForApp(appId ?? observabilityAppId, {
deepLinkId: CasesDeepLinkId.cases,
path: generateCaseViewPath({ detailName: theCase.id }),
})}
/>,
{ theme$: theme?.theme$ }
),
[appId, getUrlForApp, theme?.theme$]
);
const absoluteFromDate = parseRelativeDate(timeRange.from);
const absoluteToDate = parseRelativeDate(timeRange.to, { roundUp: true });
const { onCaseClicked, isCasesOpen, setIsCasesOpen, isSaving } = useAddToCase({
lensAttributes,
getToastText,
timeRange: {
from: absoluteFromDate?.toISOString() ?? '',
to: absoluteToDate?.toISOString() ?? '',
},
appId,
owner,
});
const getAllCasesSelectorModalProps: GetAllCasesSelectorModalProps = {
permissions: userCasesPermissions,
onRowClick: onCaseClicked,
owner: [owner],
onClose: () => {
setIsCasesOpen(false);
},
};
useEffect(() => {
if (autoOpen) {
setIsCasesOpen(true);
}
}, [autoOpen, setIsCasesOpen]);
useEffect(() => {
if (!isCasesOpen) {
setAutoOpen?.(false);
}
}, [isCasesOpen, setAutoOpen]);
return (
<>
{typeof autoOpen === 'undefined' && (
<EuiButtonEmpty
data-test-subj="o11yAddToCaseActionAddToCaseButton"
size="s"
isLoading={isSaving}
isDisabled={lensAttributes === null}
onClick={() => {
if (lensAttributes) {
setIsCasesOpen(true);
}
}}
>
{i18n.translate('xpack.observability.expView.heading.addToCase', {
defaultMessage: 'Add to case',
})}
</EuiButtonEmpty>
)}
{isCasesOpen &&
lensAttributes &&
cases.ui.getAllCasesSelectorModal(getAllCasesSelectorModalProps)}
</>
);
}
export function CaseToastText({ linkUrl }: { linkUrl: string }) {
return (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem>
<EuiLink data-test-subj="o11yCaseToastTextViewCaseLink" href={linkUrl} target="_blank">
{i18n.translate('xpack.observability.expView.heading.addToCase.notification.viewCase', {
defaultMessage: 'View case',
})}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -1,35 +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 React from 'react';
import { screen } from '@testing-library/dom';
import { render } from '../rtl_helpers';
import { ChartCreationInfo } from './chart_creation_info';
const info = {
to: 1634071132571,
from: 1633406400000,
lastUpdated: 1634071140788,
};
describe('ChartCreationInfo', () => {
it('renders chart creation info', async () => {
render(<ChartCreationInfo {...info} />);
expect(screen.getByText('Chart created')).toBeInTheDocument();
expect(screen.getByText('Oct 12, 2021 4:39 PM')).toBeInTheDocument();
expect(screen.getByText('Displaying from')).toBeInTheDocument();
expect(screen.getByText('Oct 5, 2021 12:00 AM → Oct 12, 2021 4:38 PM')).toBeInTheDocument();
});
it('does not display info when props are falsey', async () => {
render(<ChartCreationInfo />);
expect(screen.queryByText('Chart created')).not.toBeInTheDocument();
expect(screen.queryByText('Displaying from')).not.toBeInTheDocument();
});
});

View file

@ -1,61 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import moment from 'moment';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui';
import type { ChartTimeRange } from './last_updated';
export function ChartCreationInfo(props: Partial<ChartTimeRange>) {
const dateFormat = 'lll';
const from = moment(props.from).format(dateFormat);
const to = moment(props.to).format(dateFormat);
const created = moment(props.lastUpdated).format(dateFormat);
return (
<>
{props.lastUpdated && (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiText size="xs">
<FormattedMessage
id="xpack.observability.expView.seriesBuilder.creationTime"
defaultMessage="Chart created"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiText size="xs">{created}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</>
)}
{props.to && props.from && (
<>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText size="xs">
<FormattedMessage
id="xpack.observability.expView.seriesBuilder.creationContext"
defaultMessage="Displaying from"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiText size="xs">
{from} &#8594; {to}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</>
);
}

View file

@ -1,67 +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 { EuiButtonEmpty, EuiPopover, EuiCodeBlock, EuiPopoverTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { useSeriesStorage } from '../hooks/use_series_storage';
export function EmbedAction({
lensAttributes,
}: {
lensAttributes: TypedLensByValueInput['attributes'] | null;
}) {
const [isOpen, setIsOpen] = useState(false);
const { reportType, allSeries } = useSeriesStorage();
const button = (
<EuiButtonEmpty
data-test-subj="o11yEmbedActionButton"
size="s"
isDisabled={lensAttributes === null}
onClick={() => {
setIsOpen(!isOpen);
}}
>
{EMBED_LABEL}
</EuiButtonEmpty>
);
return (
<EuiPopover button={button} isOpen={isOpen} closePopover={() => setIsOpen(false)}>
<EuiPopoverTitle>{EMBED_TITLE_LABEL}</EuiPopoverTitle>
<EuiCodeBlock
language="jsx"
fontSize="m"
paddingSize="m"
isCopyable={true}
style={{ width: 500 }}
>
{`const { observability } = useKibana<>().services;
const { ExploratoryViewEmbeddable } = observability;
<ExploratoryViewEmbeddable
customHeight={'300px'}
reportType="${reportType}"
attributes={${JSON.stringify(allSeries, null, 2)}}
/>
`}
</EuiCodeBlock>
</EuiPopover>
);
}
const EMBED_TITLE_LABEL = i18n.translate('xpack.observability.expView.heading.embedTitle', {
defaultMessage: 'Embed Exploratory view (Dev only feature)',
});
const EMBED_LABEL = i18n.translate('xpack.observability.expView.heading.embed', {
defaultMessage: 'Embed <></>',
});

View file

@ -1,72 +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 React, { useEffect, useState } from 'react';
import moment from 'moment';
import styled from 'styled-components';
import { EuiIcon, EuiText, EuiToolTip } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { ChartCreationInfo } from './chart_creation_info';
export interface ChartTimeRange {
lastUpdated: number;
to?: number;
from?: number;
}
interface Props {
chartTimeRange?: ChartTimeRange;
}
export function LastUpdated({ chartTimeRange }: Props) {
const { lastUpdated } = chartTimeRange || {};
const [refresh, setRefresh] = useState(() => Date.now());
useEffect(() => {
const interVal = setInterval(() => {
setRefresh(Date.now());
}, 5000);
return () => {
clearInterval(interVal);
};
}, []);
useEffect(() => {
setRefresh(Date.now());
}, [lastUpdated]);
if (!lastUpdated) {
return null;
}
const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5;
const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10;
return (
<EuiText color={isDanger ? 'danger' : isWarning ? 'warning' : 'subdued'} size="s">
<StyledToolTipWrapper
as={EuiToolTip}
position="top"
content={<ChartCreationInfo {...chartTimeRange} />}
>
<EuiIcon type="iInCircle" />
</StyledToolTipWrapper>{' '}
<FormattedMessage
id="xpack.observability.expView.lastUpdated.label"
defaultMessage="Last Updated: {updatedDate}"
values={{
updatedDate: moment(lastUpdated).from(refresh),
}}
/>
</EuiText>
);
}
export const StyledToolTipWrapper = styled.div`
min-width: 30vw;
`;

View file

@ -1,40 +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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { LastUpdated } from './last_updated';
import { useSeriesStorage } from '../hooks/use_series_storage';
export function RefreshButton() {
const { setLastRefresh, chartTimeRangeContext } = useSeriesStorage();
return (
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem style={{ textAlign: 'right', minWidth: 280 }}>
<LastUpdated chartTimeRange={chartTimeRangeContext} />
</EuiFlexItem>
<EuiFlexItem style={{ textAlign: 'right' }}>
<EuiButton
data-test-subj="o11yRefreshButtonButton"
iconType="refresh"
onClick={() => setLastRefresh(Date.now())}
>
{REFRESH_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
);
}
export const REFRESH_LABEL = i18n.translate(
'xpack.observability.overview.exploratoryView.refresh',
{
defaultMessage: 'Refresh',
}
);

View file

@ -1,81 +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 { useAddToCase } from './use_add_to_case';
import React, { useEffect } from 'react';
import { render } from '../rtl_helpers';
import { EuiButton } from '@elastic/eui';
import { fireEvent } from '@testing-library/dom';
import { act } from '@testing-library/react';
describe('useAddToCase', function () {
function setupTestComponent() {
const setData = jest.fn();
function TestComponent() {
const getToastText = jest.fn();
const result = useAddToCase({
lensAttributes: { title: 'Test lens attributes' } as any,
timeRange: { to: 'now', from: 'now-5m' },
getToastText,
});
useEffect(() => {
setData(result);
}, [result]);
return (
<span>
<EuiButton
data-test-subj="o11yTestComponentAddNewCaseButton"
onClick={() => result.onCaseClicked()}
>
Add new case
</EuiButton>
<EuiButton
data-test-subj="o11yTestComponentOnCaseClickButton"
onClick={() => result.onCaseClicked({ id: 'test' } as any)}
>
On case click
</EuiButton>
</span>
);
}
const renderSetup = render(<TestComponent />);
return { setData, ...renderSetup };
}
it('should return expected result', async function () {
const { setData, core, findByText } = setupTestComponent();
expect(setData).toHaveBeenLastCalledWith({
isCasesOpen: false,
isSaving: false,
onCaseClicked: expect.any(Function),
setIsCasesOpen: expect.any(Function),
});
await act(async () => {
fireEvent.click(await findByText('Add new case'));
});
expect(core.application?.navigateToApp).toHaveBeenCalledTimes(1);
expect(core.application?.navigateToApp).toHaveBeenCalledWith('observability', {
deepLinkId: 'cases_create',
openInNewTab: true,
});
await act(async () => {
fireEvent.click(await findByText('On case click'));
});
expect(core.http?.post).toHaveBeenCalledTimes(1);
expect(core.http?.post).toHaveBeenCalledWith('/api/cases/test/comments', {
body: '{"comment":"!{lens{\\"attributes\\":{\\"title\\":\\"Test lens attributes\\"},\\"timeRange\\":{\\"to\\":\\"now\\",\\"from\\":\\"now-5m\\"}}}","type":"user","owner":"observability"}',
});
});
});

View file

@ -1,125 +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 { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { HttpSetup, MountPoint } from '@kbn/core/public';
import { Case } from '@kbn/cases-plugin/common';
import { TypedLensByValueInput } from '@kbn/lens-plugin/public';
import { CasesDeepLinkId, DRAFT_COMMENT_STORAGE_ID } from '@kbn/cases-plugin/public';
import { useKibana } from '../../../../utils/kibana_react';
import { AddToCaseProps } from '../header/add_to_case_action';
import { observabilityFeatureId } from '../../../../../common';
async function addToCase(
http: HttpSetup,
theCase: Case,
attributes: TypedLensByValueInput['attributes'],
timeRange?: { from: string; to: string },
owner?: string
) {
const apiPath = `/api/cases/${theCase?.id}/comments`;
const vizPayload = {
attributes,
timeRange,
};
const payload = {
comment: `!{lens${JSON.stringify(vizPayload)}}`,
type: 'user',
owner: owner ?? observabilityFeatureId,
};
return http.post(apiPath, { body: JSON.stringify(payload) });
}
export const useAddToCase = ({
lensAttributes,
getToastText,
timeRange,
appId,
owner = observabilityFeatureId,
}: AddToCaseProps & {
appId?: 'securitySolutionUI' | 'observability';
getToastText: (thaCase: Case) => MountPoint<HTMLElement>;
}) => {
const [isSaving, setIsSaving] = useState(false);
const [isCasesOpen, setIsCasesOpen] = useState(false);
const {
http,
application: { navigateToApp },
notifications: { toasts },
storage,
} = useKibana().services;
const onCaseClicked = useCallback(
(theCase?: Case) => {
if (theCase && lensAttributes) {
setIsCasesOpen(false);
setIsSaving(true);
addToCase(http, theCase, lensAttributes, timeRange, owner).then(
() => {
setIsSaving(false);
toasts.addSuccess(
{
title: i18n.translate(
'xpack.observability.expView.heading.addToCase.notification',
{
defaultMessage: 'Successfully added visualization to the case: {caseTitle}',
values: { caseTitle: theCase.title },
}
),
text: getToastText(theCase),
},
{
toastLifeTimeMs: 10000,
}
);
},
(error) => {
toasts.addError(error, {
title: i18n.translate(
'xpack.observability.expView.heading.addToCase.notification.error',
{
defaultMessage: 'Failed to add visualization to the selected case.',
}
),
});
}
);
} else {
/* save lens attributes and timerange to local storage,
** so the description field will be automatically filled on case creation page.
*/
storage?.set(DRAFT_COMMENT_STORAGE_ID, {
commentId: 'description',
comment: `!{lens${JSON.stringify({
timeRange,
attributes: lensAttributes,
})}}`,
position: '',
caseTitle: '',
caseTags: '',
});
navigateToApp(appId ?? observabilityFeatureId, {
deepLinkId: CasesDeepLinkId.casesCreate,
openInNewTab: true,
});
}
},
[appId, getToastText, http, lensAttributes, navigateToApp, owner, storage, timeRange, toasts]
);
return {
onCaseClicked,
isSaving,
isCasesOpen,
setIsCasesOpen,
};
};

View file

@ -1,115 +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 React, { createContext, useContext, Context, useState, useCallback, useMemo } from 'react';
import type { IHttpFetchError } from '@kbn/core-http-browser';
import type { DataView } from '@kbn/data-views-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DataViewInsufficientAccessError } from '@kbn/data-views-plugin/common';
import { AppDataType } from '../types';
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
import {
getDataTypeIndices,
ObservabilityDataViews,
} from '../../../../utils/observability_data_views';
export interface DataViewContext {
loading: boolean;
dataViews: DataViewState;
dataViewErrors: DataViewErrors;
hasAppData: HasAppDataState;
loadDataView: (params: { dataType: AppDataType }) => void;
}
export const DataViewContext = createContext<Partial<DataViewContext>>({});
interface ProviderProps {
children: JSX.Element;
}
type HasAppDataState = Record<AppDataType, boolean | undefined>;
export type DataViewState = Record<AppDataType, DataView>;
export type DataViewErrors = Record<AppDataType, IHttpFetchError<any>>;
type LoadingState = Record<AppDataType, boolean>;
export function DataViewContextProvider({ children }: ProviderProps) {
const [loading, setLoading] = useState<LoadingState>({} as LoadingState);
const [dataViews, setDataViews] = useState<DataViewState>({} as DataViewState);
const [dataViewErrors, setDataViewErrors] = useState<DataViewErrors>({} as DataViewErrors);
const [hasAppData, setHasAppData] = useState<HasAppDataState>({} as HasAppDataState);
const {
services: { dataViews: dataViewsService },
} = useKibana<ObservabilityPublicPluginsStart>();
const loadDataView: DataViewContext['loadDataView'] = useCallback(
async ({ dataType }) => {
if (typeof hasAppData[dataType] === 'undefined' && !loading[dataType]) {
setLoading((prevState) => ({ ...prevState, [dataType]: true }));
try {
const { indices, hasData } = await getDataTypeIndices(dataType);
setHasAppData((prevState) => ({ ...prevState, [dataType]: hasData }));
if (hasData && indices) {
const obsvDataV = new ObservabilityDataViews(dataViewsService, true);
const dataV = await obsvDataV.getDataView(dataType, indices);
setDataViews((prevState) => ({ ...prevState, [dataType]: dataV }));
}
setLoading((prevState) => ({ ...prevState, [dataType]: false }));
} catch (e) {
if (
e instanceof DataViewInsufficientAccessError ||
(e as IHttpFetchError).body === 'Forbidden'
) {
setDataViewErrors((prevState) => ({ ...prevState, [dataType]: e }));
}
setLoading((prevState) => ({ ...prevState, [dataType]: false }));
}
}
},
[dataViewsService, hasAppData, loading]
);
return (
<DataViewContext.Provider
value={{
hasAppData,
dataViews,
loadDataView,
dataViewErrors,
loading: !!Object.values(loading).find((loadingT) => loadingT),
}}
>
{children}
</DataViewContext.Provider>
);
}
export const useAppDataViewContext = (dataType?: AppDataType) => {
const { loading, hasAppData, loadDataView, dataViews, dataViewErrors } = useContext(
DataViewContext as unknown as Context<DataViewContext>
);
if (dataType && !dataViews?.[dataType] && !loading) {
loadDataView({ dataType });
}
return useMemo(() => {
return {
hasAppData,
loading,
dataViews,
dataViewErrors,
dataView: dataType ? dataViews?.[dataType] : undefined,
hasData: dataType ? hasAppData?.[dataType] : undefined,
loadDataView,
};
}, [dataType, hasAppData, dataViewErrors, dataViews, loadDataView, loading]);
};

View file

@ -1,93 +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 { useCallback, useEffect, useState } from 'react';
import { Filter } from '@kbn/es-query';
import { useKibana } from '../../../../utils/kibana_react';
import { SeriesConfig, SeriesUrl } from '../types';
import { useAppDataViewContext } from './use_app_data_view';
import { buildExistsFilter, urlFilterToPersistedFilter } from '../configurations/utils';
import { getFiltersFromDefs } from './use_lens_attributes';
import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants';
interface UseDiscoverLink {
seriesConfig?: SeriesConfig;
series: SeriesUrl;
}
export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => {
const kServices = useKibana().services;
const {
application: { navigateToUrl },
} = kServices;
const { dataViews } = useAppDataViewContext();
const locator = kServices.discover?.locator;
const [discoverUrl, setDiscoverUrl] = useState<string>('');
useEffect(() => {
const dataView = dataViews?.[series.dataType];
if (dataView) {
const definitions = series.reportDefinitions ?? {};
const urlFilters = (series.filters ?? []).concat(getFiltersFromDefs(definitions));
const filters = urlFilterToPersistedFilter({
dataView,
urlFilters,
initFilters: seriesConfig?.baseFilters,
}) as Filter[];
const selectedMetricField = series.selectedMetricField;
if (
selectedMetricField &&
selectedMetricField !== RECORDS_FIELD &&
selectedMetricField !== RECORDS_PERCENTAGE_FIELD
) {
filters.push(buildExistsFilter(selectedMetricField, dataView)[0]);
}
const getDiscoverUrl = async () => {
if (!locator) return;
const newUrl = await locator.getUrl({
filters,
indexPatternId: dataView?.id,
});
setDiscoverUrl(newUrl);
};
getDiscoverUrl();
}
}, [
dataViews,
series.dataType,
series.filters,
series.reportDefinitions,
series.selectedMetricField,
seriesConfig?.baseFilters,
locator,
]);
const onClick = useCallback(
(event: React.MouseEvent) => {
if (discoverUrl) {
event.preventDefault();
return navigateToUrl(discoverUrl);
}
},
[discoverUrl, navigateToUrl]
);
return {
href: discoverUrl,
onClick,
};
};

Some files were not shown because too many files have changed in this diff Show more