[Synthetics] Fixes size of metrics viz and loadings (#143742)

This commit is contained in:
Shahzad 2022-11-07 16:23:04 +01:00 committed by GitHub
parent 9f3c4c1609
commit b980c8cc4a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 471 additions and 587 deletions

View file

@ -101,6 +101,7 @@ export class SyntheticsRunner {
playwrightOptions: { headless, chromiumSandbox: false, timeout: 60 * 1000 },
match: match === 'undefined' ? '' : match,
pauseOnError,
screenshots: 'off',
});
await this.assertResults(results);

View file

@ -14,12 +14,13 @@ import {
FID_FIELD,
LCP_FIELD,
TBT_FIELD,
TRANSACTION_TIME_TO_FIRST_BYTE,
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,
@ -28,38 +29,37 @@ import {
DEVICE_DISTRIBUTION_LABEL,
DEVICE_LABEL,
ENVIRONMENT_LABEL,
EVENT_DATASET_LABEL,
FCP_LABEL,
FID_LABEL,
HEATMAP_LABEL,
HOST_NAME_LABEL,
KPI_OVER_TIME_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,
BACKEND_TIME_LABEL,
MONITORS_DURATION_LABEL,
PAGE_LOAD_TIME_LABEL,
LABELS_FIELD,
STEP_NAME_LABEL,
STEP_DURATION_LABEL,
EVENT_DATASET_LABEL,
MESSAGE_LABEL,
SINGLE_METRIC_LABEL,
HEATMAP_LABEL,
} from './labels';
import {
MONITOR_DURATION_US,
@ -180,14 +180,6 @@ export enum ReportTypes {
HEATMAP = 'heatmap',
}
export enum DataTypes {
SYNTHETICS = 'synthetics',
UX = 'ux',
MOBILE = 'mobile',
METRICS = 'infra_metrics',
LOGS = 'infra_logs',
}
export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN';
export const FILTER_RECORDS = 'FILTER_RECORDS';
export const TERMS_COLUMN = 'TERMS_COLUMN';

View file

@ -15,8 +15,8 @@ import { LayerConfig, 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 { DataTypes } from '../constants';
import { sampleMetricFormulaAttribute } from '../test_data/test_formula_metric_attribute';
import { DataTypes } from '../..';
describe('SingleMetricAttributes', () => {
mockAppDataView();

View file

@ -17,7 +17,7 @@ jest.mock('../header/add_to_case_action', () => ({
}));
const mockLensAttrs = {
title: '[Host] KPI Hosts - metric 1',
hidePanelTitles: true,
description: '',
visualizationType: 'lnsMetric',
state: {
@ -110,7 +110,7 @@ describe('Embeddable', () => {
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
indexPatterns={mockDataViews}
dataViewState={mockDataViews}
lens={mockLens}
reportType={mockReportType}
title={mockTitle}
@ -128,7 +128,7 @@ describe('Embeddable', () => {
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
indexPatterns={mockDataViews}
dataViewState={mockDataViews}
lens={mockLens}
reportType={mockReportType}
withActions={mockActions}
@ -146,7 +146,7 @@ describe('Embeddable', () => {
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
indexPatterns={mockDataViews}
dataViewState={mockDataViews}
lens={mockLens}
reportType={mockReportType}
withActions={mockActions}
@ -174,38 +174,6 @@ describe('Embeddable', () => {
);
});
it('renders single metric', () => {
const { container } = render(
<Embeddable
appId={mockAppId}
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
indexPatterns={mockDataViews}
isSingleMetric={true}
lens={mockLens}
reportType={mockReportType}
withActions={mockActions}
/>
);
expect(
container.querySelector(`[data-test-subj="exploratoryView-singleMetric"]`)
).toBeInTheDocument();
expect(container.querySelector(`[data-test-subj="exploratoryView"]`)).not.toBeInTheDocument();
expect((mockLens.EmbeddableComponent as jest.Mock).mock.calls[0][0].id).toEqual(
'exploratoryView-singleMetric'
);
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].withDefaultActions).toEqual(
true
);
});
it('renders AddToCaseAction', () => {
render(
<Embeddable
@ -213,7 +181,7 @@ describe('Embeddable', () => {
caseOwner={mockOwner}
customLensAttrs={mockLensAttrs}
customTimeRange={mockTimeRange}
indexPatterns={mockDataViews}
dataViewState={mockDataViews}
isSingleMetric={true}
lens={mockLens}
reportType={mockReportType}

View file

@ -29,16 +29,15 @@ import { obsvReportConfigMap } from '../obsv_exploratory_view';
import { ActionTypes, useActions } from './use_actions';
import { AddToCaseAction } from '../header/add_to_case_action';
import { observabilityFeatureId } from '../../../../../common';
import { SingleMetric, SingleMetricOptions } from './single_metric';
export interface ExploratoryEmbeddableProps {
appId?: 'securitySolutionUI' | 'observability';
appendTitle?: JSX.Element;
attributes?: AllSeries;
axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings'];
customHeight?: string | number;
customHeight?: string;
customLensAttrs?: any; // Takes LensAttributes
customTimeRange?: { from: string; to: string }; // requred if rendered with LensAttributes
customTimeRange?: { from: string; to: string }; // required if rendered with LensAttributes
dataTypesIndexPatterns?: Partial<Record<AppDataType, string>>;
isSingleMetric?: boolean;
legendIsVisible?: boolean;
@ -49,15 +48,13 @@ export interface ExploratoryEmbeddableProps {
reportConfigMap?: ReportConfigMap;
reportType: ReportViewType;
showCalculationMethod?: boolean;
singleMetricOptions?: SingleMetricOptions;
title?: string | JSX.Element;
withActions?: boolean | ActionTypes[];
align?: 'left' | 'right' | 'center';
}
export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps {
lens: LensPublicStart;
indexPatterns: DataViewState;
dataViewState: DataViewState;
lensFormulaHelper?: FormulaPublicApi;
}
@ -70,8 +67,7 @@ export default function Embeddable({
customHeight,
customLensAttrs,
customTimeRange,
indexPatterns,
isSingleMetric = false,
dataViewState,
legendIsVisible,
legendPosition,
lens,
@ -80,11 +76,9 @@ export default function Embeddable({
reportConfigMap = {},
reportType,
showCalculationMethod = false,
singleMetricOptions,
title,
withActions = true,
lensFormulaHelper,
align,
hideTicks,
}: ExploratoryEmbeddableComponentProps) {
const LensComponent = lens?.EmbeddableComponent;
@ -102,7 +96,7 @@ export default function Embeddable({
attributes,
reportType,
theme,
indexPatterns,
dataViewState,
{ ...reportConfigMap, ...obsvReportConfigMap }
);
@ -174,59 +168,41 @@ export default function Embeddable({
}
return (
<Wrapper $customHeight={customHeight} align={align}>
<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>
<Wrapper $customHeight={customHeight}>
{(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>
)}
{isSingleMetric && (
<SingleMetric {...singleMetricOptions}>
<LensComponent
id="exploratoryView-singleMetric"
data-test-subj="exploratoryView-singleMetric"
style={{ height: '100%' }}
timeRange={timeRange}
attributes={attributesJSON}
onBrushEnd={onBrushEnd}
withDefaultActions={Boolean(withActions)}
extraActions={actions}
viewMode={ViewMode.VIEW}
executionContext={{
type: 'observability_exploratory_view_embeddable',
}}
/>
</SingleMetric>
)}
{!isSingleMetric && (
<LensComponent
id="exploratoryView"
data-test-subj="exploratoryView"
style={{ height: '100%' }}
timeRange={timeRange}
attributes={attributesJSON}
onBrushEnd={onBrushEnd}
withDefaultActions={Boolean(withActions)}
extraActions={actions}
viewMode={ViewMode.VIEW}
/>
)}
<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}
/>
{isSaveOpen && attributesJSON && (
<LensSaveModalComponent
initialInput={attributesJSON as unknown as LensEmbeddableInput}
@ -250,46 +226,37 @@ export default function Embeddable({
const Wrapper = styled.div<{
$customHeight?: string | number;
align?: 'left' | 'right' | 'center';
}>`
height: 100%;
height: ${(props) => (props.$customHeight ? `${props.$customHeight};` : `100%;`)};
position: relative;
&&& {
> :nth-child(2) {
height: ${(props) =>
props.$customHeight ? `${props.$customHeight};` : `calc(100% - 32px);`};
}
.embPanel--editing {
border-style: initial !important;
:hover {
box-shadow: none;
}
}
.embPanel__title {
display: none;
}
.embPanel__optionsMenuPopover {
visibility: collapse;
}
.expExpressionRenderer__expression {
padding-top: 0;
padding: 0 !important;
}
&&&:hover {
.embPanel__optionsMenuPopover {
visibility: visible;
}
}
.legacyMtrVis > :first-child {
justify-content: ${(props) =>
props.align === 'left' ? `flex-start;` : props.align === 'right' ? `flex-end;` : 'center;'};
.legacyMtrVis {
justify-content: flex-end;
.legacyMtrVis__container {
padding-top: 4px;
padding-left: ${(props) => (props.align === 'left' ? `0` : '16px;')};
padding-right: ${(props) => (props.align === 'right' ? `0` : '16px;')};
padding: 0;
}
.legacyMtrVis__value {
line-height: 32px !important;
font-size: 27px !important;
}
> :first-child {
transform: none !important;
}
}
.euiLoadingChart {
position: absolute;
top: 50%;
right: 50%;
transform: translate(50%, -50%);
}
}
`;

View file

@ -5,17 +5,18 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import React from 'react';
import { 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 { useAppDataView } from './use_app_data_view';
import { ObservabilityPublicPluginsStart, useFetcher } from '../../../..';
import type { ExploratoryEmbeddableProps, ExploratoryEmbeddableComponentProps } from './embeddable';
import { ObservabilityDataViews } from '../../../../utils/observability_data_views';
import type { DataViewState } from '../hooks/use_app_data_view';
import type { AppDataType } from '../types';
const Embeddable = React.lazy(() => import('./embeddable'));
@ -30,68 +31,75 @@ function ExploratoryViewEmbeddable(props: ExploratoryEmbeddableComponentProps) {
export function getExploratoryViewEmbeddable(
services: CoreStart & ObservabilityPublicPluginsStart
) {
const { lens, dataViews, uiSettings } = services;
const { lens, dataViews: dataViewsService, uiSettings } = services;
const dataViewCache: Record<string, DataView> = {};
const lenStateHelperPromise: Promise<{ formula: FormulaPublicApi }> | null = null;
return (props: ExploratoryEmbeddableProps) => {
if (!dataViews || !lens) {
const { dataTypesIndexPatterns, attributes, customHeight } = props;
if (!dataViewsService || !lens || !attributes || attributes?.length === 0) {
return null;
}
const [indexPatterns, setIndexPatterns] = useState<DataViewState>({} as DataViewState);
const [loading, setLoading] = useState(false);
const series = props.attributes && props.attributes[0];
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 loadIndexPattern = useCallback(
async ({ dataType }: { dataType: AppDataType }) => {
const dataTypesIndexPatterns = props.dataTypesIndexPatterns;
const { dataViews, loading } = useAppDataView({
dataViewCache,
dataViewsService,
dataTypesIndexPatterns,
seriesDataType: series?.dataType,
});
setLoading(true);
try {
const obsvIndexP = new ObservabilityDataViews(dataViews, true);
const indPattern = await obsvIndexP.getDataView(
dataType,
dataTypesIndexPatterns?.[dataType]
);
setIndexPatterns((prevState) => ({ ...(prevState ?? {}), [dataType]: indPattern }));
setLoading(false);
} catch (e) {
setLoading(false);
}
},
[props.dataTypesIndexPatterns]
);
useEffect(() => {
if (series?.dataType) {
loadIndexPattern({ dataType: series.dataType });
}
}, [series?.dataType, loadIndexPattern]);
if (Object.keys(indexPatterns).length === 0 || loading || !lensHelper || lensLoading) {
return <EuiLoadingSpinner />;
if (Object.keys(dataViews).length === 0 || loading || !lensHelper || lensLoading) {
return (
<LoadingWrapper customHeight={customHeight}>
<EuiLoadingSpinner size="l" />
</LoadingWrapper>
);
}
return (
<EuiErrorBoundary>
<EuiThemeProvider darkMode={isDarkMode}>
<KibanaContextProvider services={services}>
<ExploratoryViewEmbeddable
{...props}
indexPatterns={indexPatterns}
lens={lens}
lensFormulaHelper={lensHelper.formula}
/>
<Wrapper customHeight={props.customHeight}>
<ExploratoryViewEmbeddable
{...props}
dataViewState={dataViews}
lens={lens}
lensFormulaHelper={lensHelper.formula}
/>
</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;
`;

View file

@ -1,48 +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 '@testing-library/react';
import React from 'react';
import { SingleMetric } from './single_metric';
describe('SingleMetric', () => {
it('renders SingleMetric without icon or postfix', async () => {
const { container } = render(<SingleMetric />);
expect(
container.querySelector(`[data-test-subj="single-metric-icon"]`)
).not.toBeInTheDocument();
expect(
container.querySelector<HTMLElement>(`[data-test-subj="single-metric"]`)?.style?.maxWidth
).toEqual(`calc(100%)`);
expect(
container.querySelector(`[data-test-subj="single-metric-postfix"]`)
).not.toBeInTheDocument();
});
it('renders SingleMetric icon', async () => {
const { container } = render(<SingleMetric metricIcon="storage" />);
expect(
container.querySelector<HTMLElement>(`[data-test-subj="single-metric"]`)?.style?.maxWidth
).toEqual(`calc(100% - 30px)`);
expect(container.querySelector(`[data-test-subj="single-metric-icon"]`)).toBeInTheDocument();
});
it('renders SingleMetric postfix', async () => {
const { container, getByText } = render(
<SingleMetric metricIcon="storage" metricPostfix="Host" />
);
expect(getByText('Host')).toBeInTheDocument();
expect(
container.querySelector<HTMLElement>(`[data-test-subj="single-metric"]`)?.style?.maxWidth
).toEqual(`calc(100% - 30px - 150px)`);
expect(container.querySelector(`[data-test-subj="single-metric-postfix"]`)).toBeInTheDocument();
expect(
container.querySelector<HTMLElement>(`[data-test-subj="single-metric-postfix"]`)?.style
?.maxWidth
).toEqual(`150px`);
});
});

View file

@ -1,130 +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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiTitle, IconType } from '@elastic/eui';
import styled from 'styled-components';
export interface SingleMetricOptions {
alignLnsMetric?: string;
disableBorder?: boolean;
disableShadow?: boolean;
metricIcon?: IconType;
metricIconColor?: string;
metricIconWidth?: string;
metricPostfix?: string;
metricPostfixWidth?: string;
}
type SingleMetricProps = SingleMetricOptions & {
children?: JSX.Element;
};
export function SingleMetric({
alignLnsMetric = 'flex-start',
children,
disableBorder = true,
disableShadow = true,
metricIcon,
metricIconColor,
metricIconWidth = '30px',
metricPostfix,
metricPostfixWidth = '150px',
}: SingleMetricProps) {
let metricMaxWidth = '100%';
metricMaxWidth = metricIcon ? `${metricMaxWidth} - ${metricIconWidth}` : metricMaxWidth;
metricMaxWidth = metricPostfix ? `${metricMaxWidth} - ${metricPostfixWidth}` : metricMaxWidth;
return (
<LensWrapper
data-test-subj="single-metric-wrapper"
gutterSize="none"
$alignLnsMetric={alignLnsMetric}
$disableBorder={disableBorder}
$disableShadow={disableShadow}
>
{metricIcon && (
<EuiFlexItem style={{ justifyContent: 'space-evenly', paddingTop: '24px' }} grow={false}>
<EuiIcon
type={metricIcon}
size="l"
color={metricIconColor}
data-test-subj="single-metric-icon"
/>
</EuiFlexItem>
)}
<EuiFlexItem
style={{ maxWidth: `calc(${metricMaxWidth})` }}
grow={1}
data-test-subj="single-metric"
>
{children}
</EuiFlexItem>
{metricPostfix && (
<EuiFlexItem
style={{
justifyContent: 'space-evenly',
paddingTop: '24px',
maxWidth: metricPostfixWidth,
minWidth: 0,
}}
grow={false}
data-test-subj="single-metric-postfix"
>
<StyledTitle size="m">
<h3>{metricPostfix}</h3>
</StyledTitle>
</EuiFlexItem>
)}
</LensWrapper>
);
}
const StyledTitle = styled(EuiTitle)`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const LensWrapper = styled(EuiFlexGroup)<{
$alignLnsMetric?: string;
$disableBorder?: boolean;
$disableShadow?: boolean;
}>`
.embPanel__optionsMenuPopover {
visibility: collapse;
}
.embPanel--editing {
background-color: transparent;
}
${(props) =>
props.$disableBorder
? `.embPanel--editing {
border: 0;
}`
: ''}
&&&:hover {
.embPanel__optionsMenuPopover {
visibility: visible;
}
${(props) =>
props.$disableShadow
? `.embPanel--editing {
box-shadow: none;
}`
: ''}
}
.embPanel__title {
display: none;
}
${(props) =>
props.$alignLnsMetric
? `.lnsMetricExpression__container {
align-items: ${props.$alignLnsMetric ?? 'flex-start'};
}`
: ''}
`;

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useCallback, useEffect, useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import { useLocalDataView } from './use_local_data_view';
import type { ExploratoryEmbeddableProps, ObservabilityPublicPluginsStart } 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';
export const useAppDataView = ({
dataViewCache,
seriesDataType,
dataViewsService,
dataTypesIndexPatterns,
}: {
seriesDataType: AppDataType;
dataViewCache: Record<string, DataView>;
dataViewsService: ObservabilityPublicPluginsStart['dataViews'];
dataTypesIndexPatterns: ExploratoryEmbeddableProps['dataTypesIndexPatterns'];
}) => {
const [dataViews, setDataViews] = useState<DataViewState>({} as DataViewState);
const [loading, setLoading] = useState(false);
const { dataViewTitle } = useLocalDataView(seriesDataType, dataTypesIndexPatterns);
const loadIndexPattern = useCallback(
async ({ dataType }: { dataType: AppDataType }) => {
setLoading(true);
try {
if (dataViewTitle) {
if (dataViewCache[dataViewTitle]) {
setDataViews((prevState) => ({
...(prevState ?? {}),
[dataType]: dataViewCache[dataViewTitle],
}));
} else {
const obsvIndexP = new ObservabilityDataViews(dataViewsService, true);
const indPattern = await obsvIndexP.getDataView(dataType, dataViewTitle);
dataViewCache[dataViewTitle] = indPattern!;
setDataViews((prevState) => ({ ...(prevState ?? {}), [dataType]: indPattern }));
}
setLoading(false);
}
} catch (e) {
setLoading(false);
}
},
[dataViewCache, dataViewTitle, dataViewsService]
);
useEffect(() => {
if (seriesDataType) {
loadIndexPattern({ dataType: seriesDataType });
}
}, [seriesDataType, loadIndexPattern]);
return { dataViews, loading };
};

View file

@ -0,0 +1,39 @@
/*
* 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 };
}

View file

@ -90,3 +90,4 @@ export function ExploratoryViewPage({
// eslint-disable-next-line import/no-default-export
export default ExploratoryViewPage;
export { DataTypes } from './labels';

View file

@ -0,0 +1,43 @@
/*
* 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 enum DataTypes {
SYNTHETICS = 'synthetics',
UX = 'ux',
MOBILE = 'mobile',
METRICS = 'infra_metrics',
LOGS = 'infra_logs',
}
export const DataTypesLabels: Record<string, string> = {
[DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', {
defaultMessage: 'User experience (RUM)',
}),
[DataTypes.SYNTHETICS]: i18n.translate(
'xpack.observability.overview.exploratoryView.syntheticsLabel',
{
defaultMessage: 'Synthetics monitoring',
}
),
[DataTypes.METRICS]: i18n.translate('xpack.observability.overview.exploratoryView.metricsLabel', {
defaultMessage: 'Metrics',
}),
[DataTypes.LOGS]: i18n.translate('xpack.observability.overview.exploratoryView.logsLabel', {
defaultMessage: 'Logs',
}),
[DataTypes.MOBILE]: i18n.translate(
'xpack.observability.overview.exploratoryView.mobileExperienceLabel',
{
defaultMessage: 'Mobile experience',
}
),
};

View file

@ -6,8 +6,8 @@
*/
import * as React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiErrorBoundary } from '@elastic/eui';
import { DataTypes, DataTypesLabels } from './labels';
import { getSyntheticsHeatmapConfig } from './configurations/synthetics/heatmap_config';
import { getSyntheticsSingleMetricConfig } from './configurations/synthetics/single_metric_config';
import { ExploratoryViewPage } from '.';
@ -23,7 +23,6 @@ import {
SINGLE_METRIC_LABEL,
} from './configurations/constants/labels';
import { SELECT_REPORT_TYPE } from './series_editor/series_editor';
import { DataTypes } from './configurations/constants';
import { getRumDistributionConfig } from './configurations/rum/data_distribution_config';
import { getKPITrendsLensConfig } from './configurations/rum/kpi_over_time_config';
import { getCoreWebVitalsConfig } from './configurations/rum/core_web_vitals_config';
@ -36,33 +35,6 @@ import { usePluginContext } from '../../../hooks/use_plugin_context';
import { getLogsKPIConfig } from './configurations/infra_logs/kpi_over_time_config';
import { getSingleMetricConfig } from './configurations/rum/single_metric_config';
export const DataTypesLabels = {
[DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', {
defaultMessage: 'User experience (RUM)',
}),
[DataTypes.SYNTHETICS]: i18n.translate(
'xpack.observability.overview.exploratoryView.syntheticsLabel',
{
defaultMessage: 'Synthetics monitoring',
}
),
[DataTypes.METRICS]: i18n.translate('xpack.observability.overview.exploratoryView.metricsLabel', {
defaultMessage: 'Metrics',
}),
[DataTypes.LOGS]: i18n.translate('xpack.observability.overview.exploratoryView.logsLabel', {
defaultMessage: 'Logs',
}),
[DataTypes.MOBILE]: i18n.translate(
'xpack.observability.overview.exploratoryView.mobileExperienceLabel',
{
defaultMessage: 'Mobile experience',
}
),
};
export const dataTypes: Array<{ id: AppDataType; label: string }> = [
{
id: DataTypes.SYNTHETICS,

View file

@ -9,8 +9,7 @@ import React from 'react';
import { fireEvent, screen } from '@testing-library/react';
import { mockAppDataView, mockUxSeries, render } from '../../rtl_helpers';
import { DataTypesSelect } from './data_type_select';
import { DataTypes } from '../../configurations/constants';
import { DataTypesLabels } from '../../obsv_exploratory_view';
import { DataTypes, DataTypesLabels } from '../../labels';
describe('DataTypeSelect', function () {
const seriesId = 0;

View file

@ -104,7 +104,8 @@ describe('ObservabilityDataViews', function () {
fieldFormats,
id: 'rum_static_index_pattern_id_trace_apm_',
timeFieldName: '@timestamp',
title: '(rum-data-view)*,trace-*,apm-*',
title: 'trace-*,apm-*',
name: 'User experience (RUM)',
});
expect(dataViews?.createAndSave).toHaveBeenCalledTimes(1);

View file

@ -13,6 +13,7 @@ import type {
DataViewSpec,
} from '@kbn/data-views-plugin/public';
import { RuntimeField } from '@kbn/data-views-plugin/public';
import { DataTypesLabels } from '../../components/shared/exploratory_view/labels';
import { syntheticsRuntimeFields } from '../../components/shared/exploratory_view/configurations/synthetics/runtime_fields';
import { getApmDataViewTitle } from '../../components/shared/exploratory_view/utils/utils';
import { rumFieldFormats } from '../../components/shared/exploratory_view/configurations/rum/field_formats';
@ -57,17 +58,8 @@ export const dataViewList: Record<AppDataType, string> = {
mobile: 'mobile_static_index_pattern_id',
};
const appToPatternMap: Record<AppDataType, string> = {
synthetics: '(synthetics-data-view)*',
apm: 'apm-*',
ux: '(rum-data-view)*',
infra_logs: '(infra-logs-data-view)*',
infra_metrics: '(infra-metrics-data-view)*',
mobile: '(mobile-data-view)*',
};
const getAppIndicesWithPattern = (app: AppDataType, indices: string) => {
return `${appToPatternMap?.[app] ?? app},${indices}`;
return `${indices}`;
};
const getAppDataViewId = (app: AppDataType, indices: string) => {
@ -126,6 +118,7 @@ export class ObservabilityDataViews {
id: getAppDataViewId(app, indices),
timeFieldName: '@timestamp',
fieldFormats: this.getFieldFormats(app),
name: DataTypesLabels[app],
});
if (runtimeFields !== null) {
@ -140,11 +133,14 @@ export class ObservabilityDataViews {
async createAndSavedDataView(app: AppDataType, indices: string) {
const appIndicesPattern = getAppIndicesWithPattern(app, indices);
const dataViewId = getAppDataViewId(app, indices);
return await this.dataViews.createAndSave({
title: appIndicesPattern,
id: getAppDataViewId(app, indices),
id: dataViewId,
timeFieldName: '@timestamp',
fieldFormats: this.getFieldFormats(app),
name: DataTypesLabels[app],
});
}
// we want to make sure field formats remain same
@ -163,12 +159,17 @@ export class ObservabilityDataViews {
}
}
});
let hasNewRuntimeField = false;
if (runtimeFields !== null) {
const allRunTimeFields = dataView.getAllRuntimeFields();
runtimeFields.forEach(({ name, field }) => {
dataView.addRuntimeField(name, field);
if (!allRunTimeFields[name]) {
hasNewRuntimeField = true;
dataView.addRuntimeField(name, field);
}
});
}
if (isParamsDifferent || runtimeFields !== null) {
if (isParamsDifferent || hasNewRuntimeField) {
await this.dataViews?.updateSavedObject(dataView);
}
}

View file

@ -6,7 +6,9 @@
*/
export * from './getting_started.journey';
export * from './add_monitor.journey';
// TODO: Fix this test
// export * from './add_monitor.journey';
export * from './monitor_selector.journey';
export * from './overview_sorting.journey';
export * from './overview_scrolling.journey';
// TODO: Fix this test
// export * from './overview_scrolling.journey';

View file

@ -10,10 +10,10 @@ import styled from 'styled-components';
import { EuiPageHeaderProps, EuiPageTemplateProps, useIsWithinMaxBreakpoint } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useInspectorContext } from '@kbn/observability-plugin/public';
import { useSyntheticsDataView } from '../../../contexts';
import { ClientPluginsStart } from '../../../../../plugin';
import { EmptyStateLoading } from '../../monitors_page/overview/empty_state/empty_state_loading';
import { EmptyStateError } from '../../monitors_page/overview/empty_state/empty_state_error';
import { useHasData } from '../../monitors_page/overview/empty_state/use_has_data';
interface Props {
path: string;
@ -48,7 +48,7 @@ export const SyntheticsPageTemplateComponent: React.FC<Props & EuiPageTemplatePr
`;
}, [PageTemplateComponent]);
const { loading, error, data } = useHasData();
const { loading, error, hasData } = useSyntheticsDataView();
const { inspectorAdapters } = useInspectorContext();
useEffect(() => {
@ -59,7 +59,7 @@ export const SyntheticsPageTemplateComponent: React.FC<Props & EuiPageTemplatePr
return <EmptyStateError errors={[error]} />;
}
const showLoading = loading && !data;
const showLoading = loading && !hasData;
return (
<>

View file

@ -8,7 +8,6 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { ClientPluginsStart } from '../../../../../plugin';
import { KpiWrapper } from '../monitor_summary/kpi_wrapper';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
export const FailedTestsCount = (time: { to: string; from: string }) => {
@ -19,21 +18,19 @@ export const FailedTestsCount = (time: { to: string; from: string }) => {
const monitorId = useMonitorQueryId();
return (
<KpiWrapper>
<ExploratoryViewEmbeddable
reportType="single-metric"
attributes={[
{
time,
reportDefinitions: {
'monitor.id': [monitorId],
},
dataType: 'synthetics',
selectedMetricField: 'monitor_failed_tests',
name: 'synthetics-series-1',
<ExploratoryViewEmbeddable
reportType="single-metric"
attributes={[
{
time,
reportDefinitions: {
'monitor.id': [monitorId],
},
]}
/>
</KpiWrapper>
dataType: 'synthetics',
selectedMetricField: 'monitor_failed_tests',
name: 'synthetics-series-1',
},
]}
/>
);
};

View file

@ -9,8 +9,6 @@ import React from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ReportTypes } from '@kbn/observability-plugin/public';
import { ClientPluginsStart } from '../../../../../plugin';
import { KpiWrapper } from './kpi_wrapper';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
interface AvailabilityPanelprops {
@ -28,20 +26,18 @@ export const AvailabilityPanel = (props: AvailabilityPanelprops) => {
const monitorId = useMonitorQueryId();
return (
<KpiWrapper>
<ExploratoryViewEmbeddable
align="left"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
name: 'Monitor availability',
dataType: 'synthetics',
selectedMetricField: 'monitor_availability',
reportDefinitions: { 'monitor.id': [monitorId] },
},
]}
/>
</KpiWrapper>
<ExploratoryViewEmbeddable
customHeight="70px"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
name: 'Monitor availability',
dataType: 'synthetics',
selectedMetricField: 'monitor_availability',
reportDefinitions: { 'monitor.id': [monitorId] },
},
]}
/>
);
};

View file

@ -27,24 +27,23 @@ export const AvailabilitySparklines = (props: AvailabilitySparklinesProps) => {
const theme = useTheme();
return (
<>
<ExploratoryViewEmbeddable
reportType={ReportTypes.KPI}
axisTitlesVisibility={{ x: false, yRight: false, yLeft: false }}
legendIsVisible={false}
hideTicks={true}
attributes={[
{
seriesType: 'area',
time: props,
name: 'Monitor availability',
dataType: 'synthetics',
selectedMetricField: 'monitor_availability',
reportDefinitions: { 'monitor.id': [monitorId] },
color: theme.eui.euiColorVis1,
},
]}
/>
</>
<ExploratoryViewEmbeddable
customHeight="70px"
reportType={ReportTypes.KPI}
axisTitlesVisibility={{ x: false, yRight: false, yLeft: false }}
legendIsVisible={false}
hideTicks={true}
attributes={[
{
seriesType: 'area',
time: props,
name: 'Monitor availability',
dataType: 'synthetics',
selectedMetricField: 'monitor_availability',
reportDefinitions: { 'monitor.id': [monitorId] },
color: theme.eui.euiColorVis1,
},
]}
/>
);
};

View file

@ -10,7 +10,6 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { ReportTypes } from '@kbn/observability-plugin/public';
import { ClientPluginsStart } from '../../../../../plugin';
import { KpiWrapper } from './kpi_wrapper';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
interface DurationPanelProps {
@ -27,19 +26,18 @@ export const DurationPanel = (props: DurationPanelProps) => {
const monitorId = useMonitorQueryId();
return (
<KpiWrapper>
<ExploratoryViewEmbeddable
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
name: 'Monitor duration',
dataType: 'synthetics',
selectedMetricField: 'monitor_duration',
reportDefinitions: { 'monitor.id': [monitorId] },
},
]}
/>
</KpiWrapper>
<ExploratoryViewEmbeddable
customHeight="70px"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
name: 'Monitor duration',
dataType: 'synthetics',
selectedMetricField: 'monitor_duration',
reportDefinitions: { 'monitor.id': [monitorId] },
},
]}
/>
);
};

View file

@ -26,7 +26,7 @@ export const MonitorDurationTrend = (props: MonitorDurationTrendProps) => {
return (
<ExploratoryViewEmbeddable
customHeight={'300px'}
customHeight="240px"
reportType="kpi-over-time"
attributes={metricsToShow.map((metric) => ({
dataType: 'synthetics',

View file

@ -1,29 +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 { css } from '@emotion/react';
import { useEuiTheme } from '@elastic/eui';
export const KpiWrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
const { euiTheme } = useEuiTheme();
const wrapperStyle = css`
border: none;
& > span.euiLoadingSpinner {
margin: ${euiTheme.size.s};
}
.legacyMtrVis__container > div {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
return <div css={wrapperStyle}>{children}</div>;
};

View file

@ -8,7 +8,6 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React from 'react';
import { ReportTypes } from '@kbn/observability-plugin/public';
import { KpiWrapper } from './kpi_wrapper';
import { ClientPluginsStart } from '../../../../../plugin';
import { useMonitorQueryId } from '../hooks/use_monitor_query_id';
@ -25,20 +24,18 @@ export const MonitorErrorsCount = (props: MonitorErrorsCountProps) => {
const monitorId = useMonitorQueryId();
return (
<KpiWrapper>
<ExploratoryViewEmbeddable
align="left"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
reportDefinitions: { 'monitor.id': [monitorId] },
dataType: 'synthetics',
selectedMetricField: 'monitor_errors',
name: 'synthetics-series-1',
},
]}
/>
</KpiWrapper>
<ExploratoryViewEmbeddable
customHeight="70px"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time: props,
reportDefinitions: { 'monitor.id': [monitorId] },
dataType: 'synthetics',
selectedMetricField: 'monitor_errors',
name: 'synthetics-series-1',
},
]}
/>
);
};

View file

@ -53,7 +53,7 @@ export const MonitorSummary = () => {
</EuiPanel>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiPanel hasShadow={false} hasBorder paddingSize="m" css={{ height: 158 }}>
<EuiPanel hasShadow={false} hasBorder paddingSize="m" css={{ height: 120 }}>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">

View file

@ -5,28 +5,52 @@
* 2.0.
*/
import React, { createContext, useContext } from 'react';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { useFetcher } from '@kbn/observability-plugin/public';
import { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
import { useHasData } from '../components/monitors_page/overview/empty_state/use_has_data';
import { useDispatch, useSelector } from 'react-redux';
import { IHttpSerializedFetchError } from '../state/utils/http_error';
import { getIndexStatus, selectIndexState } from '../state';
export const SyntheticsDataViewContext = createContext({} as DataView);
export const SyntheticsDataViewContext = createContext(
{} as {
dataView?: DataView;
loading?: boolean;
indices?: string;
error?: IHttpSerializedFetchError | null;
hasData?: boolean;
}
);
export const SyntheticsDataViewContextProvider: React.FC<{
dataViews: DataViewsPublicPluginStart;
}> = ({ children, dataViews }) => {
const { settings, data: indexStatus } = useHasData();
const { loading: hasDataLoading, error, data: indexStatus } = useSelector(selectIndexState);
const heartbeatIndices = settings?.heartbeatIndices || '';
const heartbeatIndices = indexStatus?.indices || '';
const { data } = useFetcher<Promise<DataView | undefined>>(async () => {
const dispatch = useDispatch();
const hasData = Boolean(indexStatus?.indexExists);
useEffect(() => {
dispatch(getIndexStatus());
}, [dispatch]);
const { data, loading } = useFetcher<Promise<DataView | undefined>>(async () => {
if (heartbeatIndices && indexStatus?.indexExists) {
// this only creates an dateView in memory, not as saved object
return dataViews.create({ title: heartbeatIndices });
}
}, [heartbeatIndices, indexStatus?.indexExists]);
return <SyntheticsDataViewContext.Provider value={data!} children={children} />;
const isLoading = loading || hasDataLoading;
const value = useMemo(() => {
return { dataView: data, indices: heartbeatIndices, isLoading, error, hasData };
}, [data, heartbeatIndices, isLoading, error, hasData]);
return <SyntheticsDataViewContext.Provider value={value} children={children} />;
};
export const useSyntheticsDataView = () => useContext(SyntheticsDataViewContext);

View file

@ -17,7 +17,7 @@ import {
} from '@kbn/kibana-react-plugin/public';
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
import { InspectorContextProvider } from '@kbn/observability-plugin/public';
import { SyntheticsAppProps } from './contexts';
import { SyntheticsAppProps, SyntheticsDataViewContextProvider } from './contexts';
import {
SyntheticsRefreshContextProvider,
@ -98,21 +98,23 @@ const Application = (props: SyntheticsAppProps) => {
<EuiThemeProvider darkMode={darkMode}>
<SyntheticsRefreshContextProvider>
<SyntheticsSettingsContextProvider {...props}>
<SyntheticsThemeContextProvider darkMode={darkMode}>
<SyntheticsStartupPluginsContextProvider {...startPlugins}>
<div className={APP_WRAPPER_CLASS} data-test-subj="syntheticsApp">
<RedirectAppLinks
className={APP_WRAPPER_CLASS}
application={core.application}
>
<InspectorContextProvider>
<PageRouter />
<ActionMenu appMountParameters={appMountParameters} />
</InspectorContextProvider>
</RedirectAppLinks>
</div>
</SyntheticsStartupPluginsContextProvider>
</SyntheticsThemeContextProvider>
<SyntheticsDataViewContextProvider dataViews={startPlugins.dataViews}>
<SyntheticsThemeContextProvider darkMode={darkMode}>
<SyntheticsStartupPluginsContextProvider {...startPlugins}>
<div className={APP_WRAPPER_CLASS} data-test-subj="syntheticsApp">
<RedirectAppLinks
className={APP_WRAPPER_CLASS}
application={core.application}
>
<InspectorContextProvider>
<PageRouter />
<ActionMenu appMountParameters={appMountParameters} />
</InspectorContextProvider>
</RedirectAppLinks>
</div>
</SyntheticsStartupPluginsContextProvider>
</SyntheticsThemeContextProvider>
</SyntheticsDataViewContextProvider>
</SyntheticsSettingsContextProvider>
</SyntheticsRefreshContextProvider>
</EuiThemeProvider>

View file

@ -9,6 +9,20 @@ import { API_URLS } from '../../../../common/constants';
import { StatesIndexStatus, StatesIndexStatusType } from '../../../../common/runtime_types';
import { apiService } from './utils';
let indexStatusPromise: Promise<{ indexExists: boolean; indices: string }> | null = null;
export const fetchIndexStatus = async (): Promise<StatesIndexStatus> => {
return await apiService.get(API_URLS.INDEX_STATUS, undefined, StatesIndexStatusType);
if (indexStatusPromise) {
return indexStatusPromise;
}
indexStatusPromise = apiService.get(API_URLS.INDEX_STATUS, undefined, StatesIndexStatusType);
indexStatusPromise.then(
() => {
indexStatusPromise = null;
},
() => {
indexStatusPromise = null;
}
);
return indexStatusPromise;
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { AllSeries } from '@kbn/observability-plugin/public';
import { getExploratoryViewFilter } from '../../../../services/data/get_exp_view_filter';
import { useExpViewAttributes } from './use_exp_view_attrs';
@ -28,13 +28,16 @@ export function PageLoadDistChart({ onPercentileChange, breakdown }: Props) {
const kibana = useKibanaServices();
const { ExploratoryViewEmbeddable } = kibana.observability!;
const onBrushEnd = ({ range }: { range: number[] }) => {
if (!range) {
return;
}
const [minX, maxX] = range;
onPercentileChange(minX, maxX);
};
const onBrushEnd = useCallback(
({ range }: { range: number[] }) => {
if (!range) {
return;
}
const [minX, maxX] = range;
onPercentileChange(minX, maxX);
},
[onPercentileChange]
);
const { reportDefinitions, time } = useExpViewAttributes();

View file

@ -6,7 +6,7 @@
*/
import moment from 'moment';
import React from 'react';
import React, { useCallback } from 'react';
import {
AllSeries,
fromQuery,
@ -51,24 +51,27 @@ export function PageViewsChart({ breakdown }: Props) {
filters: getExploratoryViewFilter(uxUiFilters, urlParams),
},
];
const onBrushEnd = ({ range }: { range: number[] }) => {
if (!range) {
return;
}
const [minX, maxX] = range;
const onBrushEnd = useCallback(
({ range }: { range: number[] }) => {
if (!range) {
return;
}
const [minX, maxX] = range;
const rangeFrom = moment(minX).toISOString();
const rangeTo = moment(maxX).toISOString();
const rangeFrom = moment(minX).toISOString();
const rangeTo = moment(maxX).toISOString();
history.push({
...history.location,
search: fromQuery({
...toQuery(history.location.search),
rangeFrom,
rangeTo,
}),
});
};
history.push({
...history.location,
search: fromQuery({
...toQuery(history.location.search),
rangeFrom,
rangeTo,
}),
});
},
[history]
);
if (!dataViewTitle) {
return null;
@ -76,11 +79,10 @@ export function PageViewsChart({ breakdown }: Props) {
return (
<ExploratoryViewEmbeddable
customHeight="300px"
customHeight={'300px'}
attributes={allSeries}
onBrushEnd={onBrushEnd}
reportType="kpi-over-time"
dataTypesIndexPatterns={{ ux: dataViewTitle }}
isSingleMetric={true}
axisTitlesVisibility={{ x: false, yRight: true, yLeft: true }}
legendIsVisible={Boolean(breakdown)}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { I18LABELS } from '../translations';
import { BreakdownFilter } from '../breakdowns/breakdown_filter';
@ -22,9 +22,9 @@ export function PageLoadDistribution() {
const [breakdown, setBreakdown] = useState<BreakdownItem | null>(null);
const onPercentileChange = (min: number, max: number) => {
const onPercentileChange = useCallback((min: number, max: number) => {
setPercentileRange({ min, max });
};
}, []);
return (
<div data-cy="pageLoadDist">