[Synthetics] Fix overview page vizs for large number of monitors !! (#199512)

## Summary

Fixes https://github.com/elastic/kibana/issues/187264 !!

Apply filters directly instead of passing each monitor id !!

### Testing

No special testing is needed, other than make sure, alerts/errors vizs
continue to work as expected !!

<img width="1726" alt="image"
src="https://github.com/user-attachments/assets/9c1889a5-4822-442b-97af-c2a4084c4503">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Shahzad 2024-11-21 08:27:41 +01:00 committed by GitHub
parent 764abe6599
commit 944e6fa037
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 410 additions and 203 deletions

View file

@ -7,7 +7,7 @@
import { i18n } from '@kbn/i18n';
import { capitalize } from 'lodash';
import { ExistsFilter, isExistsFilter } from '@kbn/es-query';
import { ExistsFilter, Filter, isExistsFilter } from '@kbn/es-query';
import {
AvgIndexPatternColumn,
CardinalityIndexPatternColumn,
@ -41,6 +41,7 @@ import type { DataView } from '@kbn/data-views-plugin/common';
import { PersistableFilter } from '@kbn/lens-plugin/common';
import { DataViewSpec } from '@kbn/data-views-plugin/common';
import { LegendSize } from '@kbn/visualizations-plugin/common/constants';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { urlFiltersToKueryString } from '../utils/stringify_kueries';
import {
FILTER_RECORDS,
@ -169,17 +170,20 @@ export class LensAttributes {
globalFilter?: { query: string; language: string };
reportType: string;
lensFormulaHelper?: FormulaPublicApi;
dslFilters?: QueryDslQueryContainer[];
constructor(
layerConfigs: LayerConfig[],
reportType: string,
lensFormulaHelper?: FormulaPublicApi
lensFormulaHelper?: FormulaPublicApi,
dslFilters?: QueryDslQueryContainer[]
) {
this.layers = {};
this.seriesReferenceLines = {};
this.reportType = reportType;
this.lensFormulaHelper = lensFormulaHelper;
this.isMultiSeries = layerConfigs.length > 1;
this.dslFilters = dslFilters;
layerConfigs.forEach(({ seriesConfig, operationType }) => {
if (operationType && reportType !== ReportTypes.SINGLE_METRIC) {
@ -1267,6 +1271,31 @@ export class LensAttributes {
return { internalReferences, adHocDataViews };
}
getFilters(): Filter[] {
const { internalReferences } = this.getReferences();
const dslFilters = this.dslFilters;
if (!dslFilters) {
return [];
}
return dslFilters.map((filter) => {
return {
meta: {
index: internalReferences?.[0].id,
type: 'query_string',
disabled: false,
negate: false,
alias: null,
key: 'query',
},
$state: {
store: 'appState',
},
query: filter,
} as Filter;
});
}
getJSON(
visualizationType: 'lnsXY' | 'lnsLegacyMetric' | 'lnsHeatmap' = 'lnsXY',
lastRefresh?: number
@ -1290,7 +1319,7 @@ export class LensAttributes {
},
visualization: this.visualization,
query: query || { query: '', language: 'kuery' },
filters: [],
filters: this.getFilters(),
},
};
}

View file

@ -10,6 +10,7 @@ import { FormulaPublicApi, MetricState, OperationType } from '@kbn/lens-plugin/p
import type { DataView } from '@kbn/data-views-plugin/common';
import { Query } from '@kbn/es-query';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { getColorPalette } from '../synthetics/single_metric_config';
import { FORMULA_COLUMN, RECORDS_FIELD } from '../constants';
import { ColumnFilter, MetricOption } from '../../types';
@ -28,9 +29,10 @@ export class SingleMetricLensAttributes extends LensAttributes {
constructor(
layerConfigs: LayerConfig[],
reportType: string,
lensFormulaHelper: FormulaPublicApi
lensFormulaHelper: FormulaPublicApi,
dslFilters?: QueryDslQueryContainer[]
) {
super(layerConfigs, reportType, lensFormulaHelper);
super(layerConfigs, reportType, lensFormulaHelper, dslFilters);
this.layers = {};
this.reportType = reportType;
@ -145,7 +147,7 @@ export class SingleMetricLensAttributes extends LensAttributes {
? {
id: 'percent',
params: {
decimals: 1,
decimals: 3,
},
}
: undefined,

View file

@ -106,7 +106,7 @@ export function getSyntheticsKPIConfig({ dataView }: ConfigProps): SeriesConfig
label: 'Monitor Errors',
id: 'monitor_errors',
columnType: OPERATION_COLUMN,
field: 'monitor.check_group',
field: 'state.id',
columnFilters: [
{
language: 'kuery',

View file

@ -16,8 +16,7 @@ import { ConfigProps, SeriesConfig } from '../../types';
import { FieldLabels, FORMULA_COLUMN, RECORDS_FIELD } from '../constants';
import { buildExistsFilter } from '../utils';
export const FINAL_SUMMARY_KQL =
'summary: * and (summary.final_attempt: true or not summary.final_attempt: *)';
export const FINAL_SUMMARY_KQL = 'summary.final_attempt: true';
export function getSyntheticsSingleMetricConfig({ dataView }: ConfigProps): SeriesConfig {
return {
defaultSeriesType: 'line',

View file

@ -50,7 +50,7 @@ export const sampleMetricFormulaAttribute = {
format: {
id: 'percent',
params: {
decimals: 1,
decimals: 3,
},
},
formula: "1- (count(kql='summary.down > 0') / count())",

View file

@ -19,6 +19,7 @@ import { ViewMode } from '@kbn/embeddable-plugin/common';
import { observabilityFeatureId } from '@kbn/observability-shared-plugin/public';
import styled from 'styled-components';
import { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { useEBTTelemetry } from '../hooks/use_ebt_telemetry';
import { AllSeries } from '../../../..';
import { AppDataType, ReportViewType } from '../types';
@ -57,6 +58,7 @@ export interface ExploratoryEmbeddableProps {
lineHeight?: number;
dataTestSubj?: string;
searchSessionId?: string;
dslFilters?: QueryDslQueryContainer[];
}
export interface ExploratoryEmbeddableComponentProps extends ExploratoryEmbeddableProps {

View file

@ -21,6 +21,7 @@ export const useEmbeddableAttributes = ({
reportType,
reportConfigMap = {},
lensFormulaHelper,
dslFilters,
}: ExploratoryEmbeddableComponentProps) => {
const spaceId = useKibanaSpace();
const theme = useTheme();
@ -40,7 +41,8 @@ export const useEmbeddableAttributes = ({
const lensAttributes = new SingleMetricLensAttributes(
layerConfigs,
reportType,
lensFormulaHelper!
lensFormulaHelper!,
dslFilters
);
return lensAttributes?.getJSON('lnsLegacyMetric');
} else if (reportType === ReportTypes.HEATMAP) {
@ -51,7 +53,12 @@ export const useEmbeddableAttributes = ({
);
return lensAttributes?.getJSON('lnsHeatmap');
} else {
const lensAttributes = new LensAttributes(layerConfigs, reportType, lensFormulaHelper);
const lensAttributes = new LensAttributes(
layerConfigs,
reportType,
lensFormulaHelper,
dslFilters
);
return lensAttributes?.getJSON();
}
} catch (error) {
@ -60,6 +67,7 @@ export const useEmbeddableAttributes = ({
}, [
attributes,
dataViewState,
dslFilters,
lensFormulaHelper,
reportConfigMap,
reportType,

View file

@ -112,3 +112,18 @@ export const getTimeSpanFilter = () => ({
},
},
});
export const getQueryFilters = (query: string) => ({
query_string: {
query: `${query}`,
fields: [
'monitor.name.text',
'tags',
'observer.geo.name',
'observer.name',
'urls',
'hosts',
'monitor.project.id',
],
},
});

View file

@ -0,0 +1,124 @@
/*
* 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 { renderHook } from '@testing-library/react-hooks';
import * as spaceHook from '../../../../../hooks/use_kibana_space';
import * as paramHook from '../../../hooks/use_url_params';
import * as redux from 'react-redux';
import { useMonitorFilters } from './use_monitor_filters';
import { WrappedHelper } from '../../../utils/testing';
describe('useMonitorFilters', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const spaceSpy = jest.spyOn(spaceHook, 'useKibanaSpace');
const paramSpy = jest.spyOn(paramHook, 'useGetUrlParams');
const selSPy = jest.spyOn(redux, 'useSelector');
it('should return an empty array when no parameters are provided', () => {
const { result } = renderHook(() => useMonitorFilters({}), { wrapper: WrappedHelper });
expect(result.current).toEqual([]);
});
it('should return filters for allIds and schedules', () => {
spaceSpy.mockReturnValue({} as any);
paramSpy.mockReturnValue({ schedules: 'daily' } as any);
selSPy.mockReturnValue({ status: { allIds: ['id1', 'id2'] } });
const { result } = renderHook(() => useMonitorFilters({}), { wrapper: WrappedHelper });
expect(result.current).toEqual([{ field: 'monitor.id', values: ['id1', 'id2'] }]);
});
it('should return filters for allIds and empty schedules', () => {
spaceSpy.mockReturnValue({} as any);
paramSpy.mockReturnValue({ schedules: [] } as any);
selSPy.mockReturnValue({ status: { allIds: ['id1', 'id2'] } });
const { result } = renderHook(() => useMonitorFilters({}), { wrapper: WrappedHelper });
expect(result.current).toEqual([]);
});
it('should return filters for project IDs', () => {
spaceSpy.mockReturnValue({ space: null } as any);
paramSpy.mockReturnValue({ projects: ['project1', 'project2'] } as any);
selSPy.mockReturnValue({ status: { allIds: [] } });
const { result } = renderHook(() => useMonitorFilters({}), { wrapper: WrappedHelper });
expect(result.current).toEqual([
{ field: 'monitor.project.id', values: ['project1', 'project2'] },
]);
});
it('should return filters for tags and locations', () => {
spaceSpy.mockReturnValue({ space: null } as any);
paramSpy.mockReturnValue({
tags: ['tag1', 'tag2'],
locations: ['location1', 'location2'],
} as any);
selSPy.mockReturnValue({ status: { allIds: [] } });
const { result } = renderHook(() => useMonitorFilters({}), { wrapper: WrappedHelper });
expect(result.current).toEqual([
{ field: 'tags', values: ['tag1', 'tag2'] },
{ field: 'observer.geo.name', values: ['location1', 'location2'] },
]);
});
it('should include space filters for alerts', () => {
spaceSpy.mockReturnValue({ space: { id: 'space1' } } as any);
paramSpy.mockReturnValue({} as any);
selSPy.mockReturnValue({ status: { allIds: [] } });
const { result } = renderHook(() => useMonitorFilters({ forAlerts: true }), {
wrapper: WrappedHelper,
});
expect(result.current).toEqual([{ field: 'kibana.space_ids', values: ['space1'] }]);
});
it('should include space filters for non-alerts', () => {
spaceSpy.mockReturnValue({ space: { id: 'space2' } } as any);
paramSpy.mockReturnValue({} as any);
selSPy.mockReturnValue({ status: { allIds: [] } });
const { result } = renderHook(() => useMonitorFilters({}), { wrapper: WrappedHelper });
expect(result.current).toEqual([{ field: 'meta.space_id', values: ['space2'] }]);
});
it('should handle a combination of parameters', () => {
spaceSpy.mockReturnValue({ space: { id: 'space3' } } as any);
paramSpy.mockReturnValue({
schedules: 'daily',
projects: ['projectA'],
tags: ['tagB'],
locations: ['locationC'],
monitorTypes: 'http',
} as any);
selSPy.mockReturnValue({ status: { allIds: ['id3', 'id4'] } });
const { result } = renderHook(() => useMonitorFilters({ forAlerts: false }), {
wrapper: WrappedHelper,
});
expect(result.current).toEqual([
{ field: 'monitor.id', values: ['id3', 'id4'] },
{ field: 'monitor.project.id', values: ['projectA'] },
{ field: 'monitor.type', values: ['http'] },
{ field: 'tags', values: ['tagB'] },
{ field: 'observer.geo.name', values: ['locationC'] },
{ field: 'meta.space_id', values: ['space3'] },
]);
});
});

View file

@ -0,0 +1,36 @@
/*
* 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 { UrlFilter } from '@kbn/exploratory-view-plugin/public';
import { useSelector } from 'react-redux';
import { isEmpty } from 'lodash';
import { useGetUrlParams } from '../../../hooks/use_url_params';
import { useKibanaSpace } from '../../../../../hooks/use_kibana_space';
import { selectOverviewStatus } from '../../../state/overview_status';
export const useMonitorFilters = ({ forAlerts }: { forAlerts?: boolean }): UrlFilter[] => {
const { space } = useKibanaSpace();
const { locations, monitorTypes, tags, projects, schedules } = useGetUrlParams();
const { status: overviewStatus } = useSelector(selectOverviewStatus);
const allIds = overviewStatus?.allIds ?? [];
return [
// since schedule isn't available in heartbeat data, in that case we rely on monitor.id
...(allIds?.length && !isEmpty(schedules) ? [{ field: 'monitor.id', values: allIds }] : []),
...(projects?.length ? [{ field: 'monitor.project.id', values: getValues(projects) }] : []),
...(monitorTypes?.length ? [{ field: 'monitor.type', values: getValues(monitorTypes) }] : []),
...(tags?.length ? [{ field: 'tags', values: getValues(tags) }] : []),
...(locations?.length ? [{ field: 'observer.geo.name', values: getValues(locations) }] : []),
...(space
? [{ field: forAlerts ? 'kibana.space_ids' : 'meta.space_id', values: [space.id] }]
: []),
];
};
const getValues = (values: string | string[]): string[] => {
return Array.isArray(values) ? values : [values];
};

View file

@ -0,0 +1,18 @@
/*
* 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 { useMemo } from 'react';
import { useGetUrlParams } from '../../../hooks';
import { getQueryFilters } from '../../../../../../common/constants/client_defaults';
export const useMonitorQueryFilters = () => {
const { query } = useGetUrlParams();
return useMemo(() => {
return query ? [getQueryFilters(query)] : undefined;
}, [query]);
};

View file

@ -36,7 +36,7 @@ export const MonitorAsyncError = () => {
defaultMessage="There was a problem running your monitors for one or more locations:"
/>
</p>
<ul>
<ul style={{ maxHeight: 100, overflow: 'auto' }}>
{Object.values(syncErrors ?? {}).map((e) => {
return (
<li key={e.locationId}>

View file

@ -73,9 +73,9 @@ export const MonitorStats = ({
<EuiFlexItem
css={{ display: 'flex', flexDirection: 'row', gap: euiTheme.size.l, height: '200px' }}
>
<MonitorTestRunsCount monitorIds={overviewStatus?.allIds ?? []} />
<MonitorTestRunsCount />
<EuiFlexItem grow={true}>
<MonitorTestRunsSparkline monitorIds={overviewStatus?.allIds ?? []} />
<MonitorTestRunsSparkline />
</EuiFlexItem>
</EuiFlexItem>
</EuiPanel>

View file

@ -11,31 +11,36 @@ import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useTheme } from '@kbn/observability-shared-plugin/public';
import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { useMonitorFilters } from '../../hooks/use_monitor_filters';
import { useRefreshedRange } from '../../../../hooks';
import { ClientPluginsStart } from '../../../../../../plugin';
import * as labels from '../labels';
import { useMonitorQueryFilters } from '../../hooks/use_monitor_query_filters';
export const MonitorTestRunsCount = ({ monitorIds }: { monitorIds: string[] }) => {
export const MonitorTestRunsCount = () => {
const {
exploratoryView: { ExploratoryViewEmbeddable },
} = useKibana<ClientPluginsStart>().services;
const theme = useTheme();
const { from, to } = useRefreshedRange(30, 'days');
const filters = useMonitorFilters({});
const queryFilter = useMonitorQueryFilters();
return (
<ExploratoryViewEmbeddable
dslFilters={queryFilter}
align="left"
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
filters,
time: { from, to },
reportDefinitions: {
'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'], // Show no data when monitorIds is empty
'monitor.type': ['http', 'tcp', 'browser', 'icmp'],
},
dataType: 'synthetics',
selectedMetricField: 'monitor_total_runs',
filters: [],
name: labels.TEST_RUNS_LABEL,
color: theme.eui.euiColorVis1,
},

View file

@ -10,11 +10,13 @@ import React, { useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useTheme } from '@kbn/observability-shared-plugin/public';
import { useMonitorQueryFilters } from '../../hooks/use_monitor_query_filters';
import { useMonitorFilters } from '../../hooks/use_monitor_filters';
import { useRefreshedRange } from '../../../../hooks';
import { ClientPluginsStart } from '../../../../../../plugin';
import * as labels from '../labels';
export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[] }) => {
export const MonitorTestRunsSparkline = () => {
const {
exploratoryView: { ExploratoryViewEmbeddable },
} = useKibana<ClientPluginsStart>().services;
@ -22,6 +24,8 @@ export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[]
const theme = useTheme();
const { from, to } = useRefreshedRange(30, 'days');
const filters = useMonitorFilters({});
const queryFilter = useMonitorQueryFilters();
const attributes = useMemo(() => {
return [
@ -29,18 +33,18 @@ export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[]
seriesType: 'area' as const,
time: { from, to },
reportDefinitions: {
'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'], // Show no data when monitorIds is empty
'monitor.type': ['http', 'tcp', 'browser', 'icmp'],
},
dataType: 'synthetics' as const,
selectedMetricField: 'total_test_runs',
filters: [],
filters,
name: labels.TEST_RUNS_LABEL,
color: theme.eui.euiColorVis1,
operationType: 'count',
},
];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [from, JSON.stringify({ ids: [...monitorIds].sort() }), theme.eui.euiColorVis1, to]);
}, [from, theme.eui.euiColorVis1, to]);
return (
<ExploratoryViewEmbeddable
@ -51,6 +55,7 @@ export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[]
hideTicks={true}
attributes={attributes}
customHeight={'68px'}
dslFilters={queryFilter}
/>
);
};

View file

@ -6,19 +6,18 @@
*/
import React, { useMemo } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSkeletonText,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useTheme } from '@kbn/observability-shared-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useSelector } from 'react-redux';
import { RECORDS_FIELD } from '@kbn/exploratory-view-plugin/public';
import { useMonitorQueryFilters } from '../../hooks/use_monitor_query_filters';
import {
SYNTHETICS_STATUS_RULE,
SYNTHETICS_TLS_RULE,
} from '../../../../../../../common/constants/synthetics_alerts';
import { useMonitorFilters } from '../../hooks/use_monitor_filters';
import { selectOverviewStatus } from '../../../../state/overview_status';
import { AlertsLink } from '../../../common/links/view_alerts';
import { useRefreshedRange, useGetUrlParams } from '../../../../hooks';
@ -64,14 +63,10 @@ export const OverviewAlerts = () => {
} = useKibana<ClientPluginsStart>().services;
const theme = useTheme();
const { status } = useSelector(selectOverviewStatus);
const filters = useMonitorFilters({ forAlerts: true });
const { locations } = useGetUrlParams();
const loading = !status?.allIds || status?.allIds.length === 0;
const monitorIds = useMonitorQueryIds();
const queryFilters = useMonitorQueryFilters();
return (
<EuiPanel hasShadow={false} paddingSize="m" hasBorder>
@ -79,68 +74,70 @@ export const OverviewAlerts = () => {
<h3>{headingText}</h3>
</EuiTitle>
<EuiSpacer size="s" />
{loading ? (
<EuiSkeletonText lines={3} />
) : (
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<ExploratoryViewEmbeddable
id="monitorActiveAlertsCount"
dataTestSubj="monitorActiveAlertsCount"
reportType="single-metric"
customHeight="70px"
attributes={[
{
dataType: 'alerts',
time: {
from,
to,
},
name: ALERTS_LABEL,
selectedMetricField: RECORDS_FIELD,
reportDefinitions: {
'kibana.alert.rule.category': ['Synthetics monitor status'],
'monitor.id': monitorIds,
...(locations?.length ? { 'observer.geo.name': locations } : {}),
},
filters: [{ field: 'kibana.alert.status', values: ['active', 'recovered'] }],
color: theme.eui.euiColorVis1,
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem grow={false}>
<ExploratoryViewEmbeddable
id="monitorActiveAlertsCount"
dataTestSubj="monitorActiveAlertsCount"
reportType="single-metric"
customHeight="70px"
dslFilters={queryFilters}
attributes={[
{
dataType: 'alerts',
time: {
from,
to,
},
]}
/>
</EuiFlexItem>
<EuiFlexItem>
<ExploratoryViewEmbeddable
id="monitorActiveAlertsOverTime"
sparklineMode
customHeight="70px"
reportType="kpi-over-time"
attributes={[
{
seriesType: 'area',
time: {
from,
to,
},
reportDefinitions: {
'kibana.alert.rule.category': ['Synthetics monitor status'],
'monitor.id': monitorIds,
...(locations?.length ? { 'observer.geo.name': locations } : {}),
},
dataType: 'alerts',
selectedMetricField: RECORDS_FIELD,
name: ALERTS_LABEL,
filters: [{ field: 'kibana.alert.status', values: ['active', 'recovered'] }],
color: theme.eui.euiColorVis1_behindText,
name: ALERTS_LABEL,
selectedMetricField: RECORDS_FIELD,
reportDefinitions: {
'kibana.alert.rule.rule_type_id': [SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE],
...(locations?.length ? { 'observer.geo.name': locations } : {}),
},
]}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} css={{ alignSelf: 'center' }}>
<AlertsLink />
</EuiFlexItem>
</EuiFlexGroup>
)}
filters: [
{ field: 'kibana.alert.status', values: ['active', 'recovered'] },
...filters,
],
color: theme.eui.euiColorVis1,
},
]}
/>
</EuiFlexItem>
<EuiFlexItem>
<ExploratoryViewEmbeddable
id="monitorActiveAlertsOverTime"
sparklineMode
customHeight="70px"
reportType="kpi-over-time"
dslFilters={queryFilters}
attributes={[
{
seriesType: 'area',
time: {
from,
to,
},
reportDefinitions: {
'kibana.alert.rule.rule_type_id': [SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE],
...(locations?.length ? { 'observer.geo.name': locations } : {}),
},
dataType: 'alerts',
selectedMetricField: RECORDS_FIELD,
name: ALERTS_LABEL,
filters: [
{ field: 'kibana.alert.status', values: ['active', 'recovered'] },
...filters,
],
color: theme.eui.euiColorVis1_behindText,
},
]}
/>
</EuiFlexItem>
<EuiFlexItem grow={false} css={{ alignSelf: 'center' }}>
<AlertsLink />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
};

View file

@ -5,62 +5,30 @@
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiSkeletonText,
EuiPanel,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import React from 'react';
import { useSelector } from 'react-redux';
import { i18n } from '@kbn/i18n';
import { useMonitorQueryIds } from '../overview_alerts';
import { selectOverviewStatus } from '../../../../../state/overview_status';
import { OverviewErrorsSparklines } from './overview_errors_sparklines';
import { useRefreshedRange, useGetUrlParams } from '../../../../../hooks';
import { useRefreshedRange } from '../../../../../hooks';
import { OverviewErrorsCount } from './overview_errors_count';
export function OverviewErrors() {
const { status } = useSelector(selectOverviewStatus);
const loading = !status?.allIds || status?.allIds.length === 0;
const { from, to } = useRefreshedRange(6, 'hours');
const { locations } = useGetUrlParams();
const monitorIds = useMonitorQueryIds();
return (
<EuiPanel hasShadow={false} hasBorder>
<EuiTitle size="xs">
<h3>{headingText}</h3>
</EuiTitle>
<EuiSpacer size="s" />
{loading ? (
<EuiSkeletonText lines={3} />
) : (
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem grow={false}>
<OverviewErrorsCount
from={from}
to={to}
monitorIds={monitorIds}
locations={locations}
/>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<OverviewErrorsSparklines
from={from}
to={to}
monitorIds={monitorIds}
locations={locations}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem grow={false}>
<OverviewErrorsCount from={from} to={to} />
</EuiFlexItem>
<EuiFlexItem grow={true}>
<OverviewErrorsSparklines from={from} to={to} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -8,27 +8,23 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useMemo } from 'react';
import { ReportTypes } from '@kbn/exploratory-view-plugin/public';
import { useMonitorFilters } from '../../../hooks/use_monitor_filters';
import { ERRORS_LABEL } from '../../../../monitor_details/monitor_summary/monitor_errors_count';
import { ClientPluginsStart } from '../../../../../../../plugin';
import { useMonitorQueryFilters } from '../../../hooks/use_monitor_query_filters';
interface MonitorErrorsCountProps {
from: string;
to: string;
locationLabel?: string;
monitorIds: string[];
locations?: string[];
}
export const OverviewErrorsCount = ({
monitorIds,
from,
to,
locations,
}: MonitorErrorsCountProps) => {
export const OverviewErrorsCount = ({ from, to }: MonitorErrorsCountProps) => {
const {
exploratoryView: { ExploratoryViewEmbeddable },
} = useKibana<ClientPluginsStart>().services;
const filters = useMonitorFilters({});
const time = useMemo(() => ({ from, to }), [from, to]);
return (
@ -36,17 +32,18 @@ export const OverviewErrorsCount = ({
id="overviewErrorsCount"
align="left"
customHeight="70px"
dslFilters={useMonitorQueryFilters()}
reportType={ReportTypes.SINGLE_METRIC}
attributes={[
{
time,
reportDefinitions: {
'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'],
...(locations?.length ? { 'observer.geo.name': locations } : {}),
'monitor.type': ['http', 'tcp', 'browser', 'icmp'],
},
dataType: 'synthetics',
selectedMetricField: 'monitor_errors',
name: ERRORS_LABEL,
filters,
},
]}
/>

View file

@ -8,20 +8,21 @@
import { useKibana } from '@kbn/kibana-react-plugin/public';
import React, { useMemo } from 'react';
import { useEuiTheme } from '@elastic/eui';
import { ClientPluginsStart } from '../../../../../../../plugin';
import { ERRORS_LABEL } from '../../../../monitor_details/monitor_summary/monitor_errors_count';
import { ClientPluginsStart } from '../../../../../../../plugin';
import { useMonitorFilters } from '../../../hooks/use_monitor_filters';
import { useMonitorQueryFilters } from '../../../hooks/use_monitor_query_filters';
interface Props {
from: string;
to: string;
monitorIds: string[];
locations?: string[];
}
export const OverviewErrorsSparklines = ({ from, to, monitorIds, locations }: Props) => {
export const OverviewErrorsSparklines = ({ from, to }: Props) => {
const {
exploratoryView: { ExploratoryViewEmbeddable },
} = useKibana<ClientPluginsStart>().services;
const filters = useMonitorFilters({});
const { euiTheme } = useEuiTheme();
const time = useMemo(() => ({ from, to }), [from, to]);
@ -33,19 +34,20 @@ export const OverviewErrorsSparklines = ({ from, to, monitorIds, locations }: Pr
axisTitlesVisibility={{ x: false, yRight: false, yLeft: false }}
legendIsVisible={false}
hideTicks={true}
dslFilters={useMonitorQueryFilters()}
attributes={[
{
time,
seriesType: 'area',
reportDefinitions: {
'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'],
...(locations?.length ? { 'observer.geo.name': locations } : {}),
'monitor.type': ['http', 'tcp', 'browser', 'icmp'],
},
dataType: 'synthetics',
selectedMetricField: 'monitor_errors',
name: ERRORS_LABEL,
color: euiTheme.colors.danger,
operationType: 'unique_count',
filters,
},
]}
/>

View file

@ -114,7 +114,6 @@ export const OverviewGrid = memo(() => {
return acc;
}, [monitorsSortedByStatus]);
const listRef: React.LegacyRef<FixedSizeList<ListItem[][]>> | undefined = React.createRef();
useEffect(() => {
dispatch(refreshOverviewTrends.get());
}, [dispatch, lastRefresh]);
@ -165,50 +164,52 @@ export const OverviewGrid = memo(() => {
minimumBatchSize={MIN_BATCH_SIZE}
threshold={LIST_THRESHOLD}
>
{({ onItemsRendered }) => (
<FixedSizeList
// pad computed height to avoid clipping last row's drop shadow
height={listHeight + 16}
width={width}
onItemsRendered={onItemsRendered}
itemSize={ITEM_HEIGHT}
itemCount={listItems.length}
itemData={listItems}
ref={listRef}
>
{({
index: listIndex,
style,
data: listData,
}: React.PropsWithChildren<ListChildComponentProps<ListItem[][]>>) => {
setCurrentIndex(listIndex);
return (
<EuiFlexGroup
data-test-subj={`overview-grid-row-${listIndex}`}
gutterSize="m"
style={{ ...style }}
>
{listData[listIndex].map((_, idx) => (
<EuiFlexItem
data-test-subj="syntheticsOverviewGridItem"
key={listIndex * ROW_COUNT + idx}
>
<MetricItem
monitor={monitorsSortedByStatus[listIndex * ROW_COUNT + idx]}
onClick={setFlyoutConfigCallback}
/>
</EuiFlexItem>
))}
{listData[listIndex].length % ROW_COUNT !== 0 &&
// Adds empty items to fill out row
Array.from({
length: ROW_COUNT - listData[listIndex].length,
}).map((_, idx) => <EuiFlexItem key={idx} />)}
</EuiFlexGroup>
);
}}
</FixedSizeList>
)}
{({ onItemsRendered, ref }) => {
return (
<FixedSizeList
// pad computed height to avoid clipping last row's drop shadow
height={listHeight + 16}
width={width}
onItemsRendered={onItemsRendered}
itemSize={ITEM_HEIGHT}
itemCount={listItems.length}
itemData={listItems}
ref={ref}
>
{({
index: listIndex,
style,
data: listData,
}: React.PropsWithChildren<ListChildComponentProps<ListItem[][]>>) => {
setCurrentIndex(listIndex);
return (
<EuiFlexGroup
data-test-subj={`overview-grid-row-${listIndex}`}
gutterSize="m"
style={{ ...style }}
>
{listData[listIndex].map((_, idx) => (
<EuiFlexItem
data-test-subj="syntheticsOverviewGridItem"
key={listIndex * ROW_COUNT + idx}
>
<MetricItem
monitor={monitorsSortedByStatus[listIndex * ROW_COUNT + idx]}
onClick={setFlyoutConfigCallback}
/>
</EuiFlexItem>
))}
{listData[listIndex].length % ROW_COUNT !== 0 &&
// Adds empty items to fill out row
Array.from({
length: ROW_COUNT - listData[listIndex].length,
}).map((_, idx) => <EuiFlexItem key={idx} />)}
</EuiFlexGroup>
);
}}
</FixedSizeList>
);
}}
</InfiniteLoader>
)}
</EuiAutoSizer>
@ -239,7 +240,6 @@ export const OverviewGrid = memo(() => {
data-test-subj="syntheticsOverviewGridButton"
onClick={() => {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
listRef.current?.scrollToItem(0);
}}
iconType="sortUp"
iconSide="right"

View file

@ -8,7 +8,7 @@ import type { Space } from '@kbn/spaces-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { useFetcher } from '@kbn/observability-shared-plugin/public';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { ClientPluginsStart } from '../plugin';
import type { ClientPluginsStart } from '../plugin';
export const useKibanaSpace = () => {
const { services } = useKibana<ClientPluginsStart>();

View file

@ -111,7 +111,7 @@ export const getMonitors = async (
sortField: parseMappingKey(sortField),
sortOrder,
searchFields: SEARCH_FIELDS,
search: query ? `${query}*` : undefined,
search: query,
filter: filtersStr,
searchAfter,
fields,