mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
764abe6599
commit
944e6fa037
22 changed files with 410 additions and 203 deletions
|
@ -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(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -50,7 +50,7 @@ export const sampleMetricFormulaAttribute = {
|
|||
format: {
|
||||
id: 'percent',
|
||||
params: {
|
||||
decimals: 1,
|
||||
decimals: 3,
|
||||
},
|
||||
},
|
||||
formula: "1- (count(kql='summary.down > 0') / count())",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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'] },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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];
|
||||
};
|
|
@ -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]);
|
||||
};
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue