mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
7534cc7851
commit
a07bdc5da9
180 changed files with 817 additions and 18916 deletions
|
@ -95,7 +95,7 @@ pageLoadAssetSize:
|
|||
newsfeed: 42228
|
||||
observability: 95000
|
||||
observabilityOnboarding: 19573
|
||||
observabilityShared: 21266
|
||||
observabilityShared: 36643
|
||||
osquery: 107090
|
||||
painlessLab: 179748
|
||||
presentationUtil: 58834
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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.',
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -56,7 +56,7 @@ export function ExploratoryViewPage({
|
|||
useBreadcrumbs(
|
||||
[
|
||||
{
|
||||
text: i18n.translate('xpack.exploratoryView.overview.exploratoryView', {
|
||||
text: i18n.translate('xpack.exploratoryView.overview', {
|
||||
defaultMessage: 'Explore data',
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -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.',
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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.',
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"data",
|
||||
"dataViews",
|
||||
"embeddable",
|
||||
"exploratoryView",
|
||||
"features",
|
||||
"files",
|
||||
"guidedOnboarding",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
## 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)
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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.' }
|
||||
);
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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)));
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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',
|
||||
}),
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 };
|
||||
};
|
|
@ -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}"`,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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}"`,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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: {} },
|
||||
},
|
||||
];
|
|
@ -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';
|
|
@ -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;
|
|
@ -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';
|
|
@ -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,
|
||||
];
|
|
@ -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';
|
|
@ -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',
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
};
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 },
|
||||
};
|
||||
}
|
|
@ -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: {},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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 },
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 };
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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' },
|
||||
};
|
||||
}
|
|
@ -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' },
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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 },
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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 },
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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 },
|
||||
};
|
||||
}
|
|
@ -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`,
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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: [],
|
||||
},
|
||||
};
|
|
@ -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',
|
||||
};
|
|
@ -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',
|
||||
};
|
|
@ -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',
|
||||
};
|
|
@ -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',
|
||||
};
|
File diff suppressed because one or more lines are too long
|
@ -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',
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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',
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -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 };
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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.',
|
||||
}
|
||||
);
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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} → {to}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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 <></>',
|
||||
});
|
|
@ -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;
|
||||
`;
|
|
@ -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',
|
||||
}
|
||||
);
|
|
@ -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"}',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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]);
|
||||
};
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue