mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Synthetics] Overview and Management filters (#149469)
Closes #135160 Fixes https://github.com/elastic/kibana/issues/146075 ## Summary Adds the Frequency and Project filter on Management page and all the filters on Overview page as well. The PR doesn't show the filters on a dialog as in the design, for the sake of utilizing existing available component and the fact that opening a dialog adds one additional step to reach filters. The applied filter pills/tags (as in the design) are also not implemented as the filter components show a highlighted number if any filter is applied. Incase this implementation is not sufficient, the same components can be converted to match the design easily. <img width="1482" alt="Screenshot 2023-01-25 at 23 18 30" src="https://user-images.githubusercontent.com/2748376/214705768-78b431a5-d0fd-4141-82f2-0e9d3af0d8ee.png"> <img width="1484" alt="Screenshot 2023-01-25 at 23 19 12" src="https://user-images.githubusercontent.com/2748376/214705792-bf62004d-7666-408b-8ca1-4f0f7520a950.png"> --------- Co-authored-by: shahzad31 <shahzad31comp@gmail.com>
This commit is contained in:
parent
e8eb04420e
commit
4795910ef3
42 changed files with 855 additions and 388 deletions
|
@ -16,7 +16,9 @@ export const FetchMonitorManagementListQueryArgsCodec = t.partial({
|
|||
searchFields: t.array(t.string),
|
||||
tags: t.array(t.string),
|
||||
locations: t.array(t.string),
|
||||
monitorType: t.array(t.string),
|
||||
monitorTypes: t.array(t.string),
|
||||
projects: t.array(t.string),
|
||||
schedules: t.array(t.string),
|
||||
});
|
||||
|
||||
export type FetchMonitorManagementListQueryArgs = t.TypeOf<
|
||||
|
@ -28,7 +30,9 @@ export const FetchMonitorOverviewQueryArgsCodec = t.partial({
|
|||
searchFields: t.array(t.string),
|
||||
tags: t.array(t.string),
|
||||
locations: t.array(t.string),
|
||||
monitorType: t.array(t.string),
|
||||
projects: t.array(t.string),
|
||||
schedules: t.array(t.string),
|
||||
monitorTypes: t.array(t.string),
|
||||
sortField: t.string,
|
||||
sortOrder: t.string,
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ export const OverviewStatusCodec = t.interface({
|
|||
disabledCount: t.number,
|
||||
upConfigs: t.record(t.string, OverviewStatusMetaDataCodec),
|
||||
downConfigs: t.record(t.string, OverviewStatusMetaDataCodec),
|
||||
allIds: t.array(t.string),
|
||||
enabledIds: t.array(t.string),
|
||||
});
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: a
|
|||
});
|
||||
|
||||
step('shows validation error on submit', async () => {
|
||||
await page.locator('.euiSideNavItem').locator('text=Synthetics').click();
|
||||
await page.click('text=Create monitor');
|
||||
|
||||
expect(await page.isVisible('text=URL is required')).toBeTruthy();
|
||||
|
|
|
@ -36,7 +36,10 @@ journey(`MonitorManagementList`, async ({ page, params }) => {
|
|||
|
||||
await addTestMonitor(params.kibanaUrl, testMonitor1);
|
||||
await addTestMonitor(params.kibanaUrl, testMonitor2);
|
||||
await addTestMonitor(params.kibanaUrl, testMonitor3);
|
||||
await addTestMonitor(params.kibanaUrl, testMonitor3, {
|
||||
type: 'browser',
|
||||
schedule: { unit: 'm', number: '5' },
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -64,17 +67,17 @@ journey(`MonitorManagementList`, async ({ page, params }) => {
|
|||
});
|
||||
|
||||
step(
|
||||
'Click [aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text=browser',
|
||||
'Click [aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text="Journey / Page"',
|
||||
async () => {
|
||||
await page.click(
|
||||
'[aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text=browser'
|
||||
'[aria-label="Use up and down arrows to move focus over options. Enter to select. Escape to collapse options."] >> text="Journey / Page"'
|
||||
);
|
||||
await page.click('[aria-label="Apply the selected filters for Type"]');
|
||||
expect(page.url()).toBe(`${pageBaseUrl}?monitorType=%5B%22browser%22%5D`);
|
||||
expect(page.url()).toBe(`${pageBaseUrl}?monitorTypes=%5B%22browser%22%5D`);
|
||||
await page.click('[placeholder="Search by name, url, host, tag, project or location"]');
|
||||
await Promise.all([
|
||||
page.waitForNavigation({
|
||||
url: `${pageBaseUrl}?monitorType=%5B%22browser%22%5D&query=3`,
|
||||
url: `${pageBaseUrl}?monitorTypes=%5B%22browser%22%5D&query=3`,
|
||||
}),
|
||||
page.fill('[placeholder="Search by name, url, host, tag, project or location"]', '3'),
|
||||
]);
|
||||
|
@ -99,4 +102,22 @@ journey(`MonitorManagementList`, async ({ page, params }) => {
|
|||
await expect(statSummaryPanel.locator('text=3').count()).resolves.toEqual(1); // Configurations
|
||||
await expect(statSummaryPanel.locator('text=0').count()).resolves.toEqual(1); // Disabled
|
||||
});
|
||||
|
||||
step('Filter by Frequency', async () => {
|
||||
const frequencyFilter = page.locator('.euiFilterButton__textShift', { hasText: 'Frequency' });
|
||||
const fiveMinuteScheduleOption = page.getByText('Every 5 minutes').first();
|
||||
|
||||
await frequencyFilter.click();
|
||||
await fiveMinuteScheduleOption.click();
|
||||
await page.getByText('Apply').click();
|
||||
|
||||
// There should be only 1 monitor with schedule 5 minutes
|
||||
await page.waitForSelector('text=1-1');
|
||||
|
||||
// Clear the filter
|
||||
await frequencyFilter.click();
|
||||
await fiveMinuteScheduleOption.click();
|
||||
await page.getByText('Apply').click();
|
||||
await page.waitForSelector('text=1-3');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { FieldValueSelection } from '@kbn/observability-plugin/public';
|
||||
import {
|
||||
getSyntheticsFilterDisplayValues,
|
||||
SyntheticsMonitorFilterItem,
|
||||
valueToLabelWithEmptyCount,
|
||||
} from './filter_fields';
|
||||
import { useGetUrlParams } from '../../../../hooks';
|
||||
import { useMonitorFiltersState } from './use_filters';
|
||||
|
||||
export const FilterButton = ({
|
||||
filter,
|
||||
handleFilterChange,
|
||||
}: {
|
||||
filter: SyntheticsMonitorFilterItem;
|
||||
handleFilterChange: ReturnType<typeof useMonitorFiltersState>['handleFilterChange'];
|
||||
}) => {
|
||||
const { label, values, field } = filter;
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const urlParams = useGetUrlParams();
|
||||
|
||||
// Transform the values to readable labels (if any) so that selected values are checked on filter dropdown
|
||||
const selectedValueLabels = getSyntheticsFilterDisplayValues(
|
||||
(urlParams[field] || []).map(valueToLabelWithEmptyCount),
|
||||
field,
|
||||
[]
|
||||
).map(({ label: selectedValueLabel }) => selectedValueLabel);
|
||||
|
||||
return (
|
||||
<FieldValueSelection
|
||||
selectedValue={selectedValueLabels}
|
||||
singleSelection={false}
|
||||
label={label}
|
||||
values={
|
||||
query
|
||||
? values.filter(({ label: str }) => str.toLowerCase().includes(query.toLowerCase()))
|
||||
: values
|
||||
}
|
||||
setQuery={setQuery}
|
||||
onChange={(selectedValues) => handleFilterChange(field, selectedValues)}
|
||||
allowExclusions={false}
|
||||
loading={false}
|
||||
asFilterButton={true}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { invert } from 'lodash';
|
||||
import { DataStream, ServiceLocations } from '../../../../../../../common/runtime_types';
|
||||
import { MonitorFilterState } from '../../../../state';
|
||||
|
||||
export type SyntheticsMonitorFilterField = keyof MonitorFilterState;
|
||||
|
||||
export interface LabelWithCountValue {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface SyntheticsMonitorFilterItem {
|
||||
label: string;
|
||||
values: LabelWithCountValue[];
|
||||
field: SyntheticsMonitorFilterField;
|
||||
}
|
||||
|
||||
export function getMonitorFilterFields(): SyntheticsMonitorFilterField[] {
|
||||
return ['tags', 'locations', 'monitorTypes', 'projects', 'schedules'];
|
||||
}
|
||||
|
||||
export type SyntheticsMonitorFilterChangeHandler = (
|
||||
field: SyntheticsMonitorFilterField,
|
||||
selectedValues: string[] | undefined
|
||||
) => void;
|
||||
|
||||
export function getSyntheticsFilterDisplayValues(
|
||||
values: LabelWithCountValue[],
|
||||
field: SyntheticsMonitorFilterField,
|
||||
locations: ServiceLocations
|
||||
) {
|
||||
switch (field) {
|
||||
case 'monitorTypes':
|
||||
return values.map(({ label, count }: { label: string; count: number }) => ({
|
||||
label: monitorTypeKeyLabelMap[label as DataStream] ?? label,
|
||||
count,
|
||||
}));
|
||||
case 'schedules':
|
||||
return values.map(({ label, count }: { label: string; count: number }) => ({
|
||||
label: i18n.translate('xpack.synthetics.monitorFilters.frequencyLabel', {
|
||||
defaultMessage: `Every {count} minutes`,
|
||||
values: { count: label },
|
||||
}),
|
||||
count,
|
||||
}));
|
||||
case 'locations':
|
||||
return values.map(({ label, count }) => {
|
||||
const foundLocation = locations.find(
|
||||
({ id: locationId, label: locationLabel }) =>
|
||||
label === locationId || label === locationLabel
|
||||
);
|
||||
return {
|
||||
label: foundLocation?.label ?? label,
|
||||
count,
|
||||
};
|
||||
});
|
||||
default:
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSyntheticsFilterKeyForLabel(value: string, field: SyntheticsMonitorFilterField) {
|
||||
switch (field) {
|
||||
case 'monitorTypes':
|
||||
return invert(monitorTypeKeyLabelMap)[value] ?? value;
|
||||
case 'schedules':
|
||||
return (value ?? '').replace(/\D/g, '');
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export const valueToLabelWithEmptyCount = (value: string): LabelWithCountValue => ({
|
||||
label: value,
|
||||
count: 0,
|
||||
});
|
||||
|
||||
const monitorTypeKeyLabelMap: Record<DataStream, string> = {
|
||||
[DataStream.BROWSER]: 'Journey / Page',
|
||||
[DataStream.HTTP]: 'HTTP',
|
||||
[DataStream.TCP]: 'TCP',
|
||||
[DataStream.ICMP]: 'ICMP',
|
||||
};
|
|
@ -10,50 +10,64 @@ import { EuiFilterGroup } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { ServiceLocations } from '../../../../../../../common/runtime_types';
|
||||
import { useFilters } from './use_filters';
|
||||
import { FilterButton } from './filter_button';
|
||||
import { selectServiceLocationsState } from '../../../../state';
|
||||
|
||||
export interface FilterItem {
|
||||
label: string;
|
||||
values: Array<{ label: string; count: number }>;
|
||||
field: 'tags' | 'status' | 'locations' | 'monitorType';
|
||||
}
|
||||
import {
|
||||
SyntheticsMonitorFilterItem,
|
||||
getSyntheticsFilterDisplayValues,
|
||||
SyntheticsMonitorFilterChangeHandler,
|
||||
} from './filter_fields';
|
||||
import { useFilters } from './use_filters';
|
||||
import { FilterButton } from './filter_button';
|
||||
|
||||
export const findLocationItem = (query: string, locations: ServiceLocations) => {
|
||||
return locations.find(({ id, label }) => query === id || label === query);
|
||||
};
|
||||
|
||||
export const FilterGroup = () => {
|
||||
export const FilterGroup = ({
|
||||
handleFilterChange,
|
||||
}: {
|
||||
handleFilterChange: SyntheticsMonitorFilterChangeHandler;
|
||||
}) => {
|
||||
const data = useFilters();
|
||||
|
||||
const { locations } = useSelector(selectServiceLocationsState);
|
||||
|
||||
const filters: FilterItem[] = [
|
||||
const filters: SyntheticsMonitorFilterItem[] = [
|
||||
{
|
||||
label: TYPE_LABEL,
|
||||
field: 'monitorType',
|
||||
values: data.types,
|
||||
field: 'monitorTypes',
|
||||
values: getSyntheticsFilterDisplayValues(data.monitorTypes, 'monitorTypes', locations),
|
||||
},
|
||||
{
|
||||
label: LOCATION_LABEL,
|
||||
field: 'locations',
|
||||
values: data.locations.map(({ label, count }) => ({
|
||||
label: findLocationItem(label, locations)?.label ?? label,
|
||||
count,
|
||||
})),
|
||||
values: getSyntheticsFilterDisplayValues(data.locations, 'locations', locations),
|
||||
},
|
||||
{
|
||||
label: TAGS_LABEL,
|
||||
field: 'tags',
|
||||
values: data.tags,
|
||||
values: getSyntheticsFilterDisplayValues(data.tags, 'tags', locations),
|
||||
},
|
||||
{
|
||||
label: SCHEDULE_LABEL,
|
||||
field: 'schedules',
|
||||
values: getSyntheticsFilterDisplayValues(data.schedules, 'schedules', locations),
|
||||
},
|
||||
];
|
||||
|
||||
if (data.projects.length > 0) {
|
||||
filters.push({
|
||||
label: PROJECT_LABEL,
|
||||
field: 'projects',
|
||||
values: getSyntheticsFilterDisplayValues(data.projects, 'projects', locations),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFilterGroup>
|
||||
{filters.map((filter, index) => (
|
||||
<FilterButton key={index} filter={filter} />
|
||||
<FilterButton key={index} filter={filter} handleFilterChange={handleFilterChange} />
|
||||
))}
|
||||
</EuiFilterGroup>
|
||||
);
|
||||
|
@ -63,6 +77,10 @@ const TYPE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.typ
|
|||
defaultMessage: `Type`,
|
||||
});
|
||||
|
||||
const PROJECT_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.projectLabel', {
|
||||
defaultMessage: `Project`,
|
||||
});
|
||||
|
||||
const LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.locationLabel', {
|
||||
defaultMessage: `Location`,
|
||||
});
|
||||
|
@ -70,3 +88,7 @@ const LOCATION_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter
|
|||
const TAGS_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.tagsLabel', {
|
||||
defaultMessage: `Tags`,
|
||||
});
|
||||
|
||||
const SCHEDULE_LABEL = i18n.translate('xpack.synthetics.monitorManagement.filter.frequencyLabel', {
|
||||
defaultMessage: `Frequency`,
|
||||
});
|
|
@ -7,17 +7,23 @@
|
|||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { FilterGroup } from './filter_group';
|
||||
import { SearchField } from '../../common/search_field';
|
||||
|
||||
export function ListFilters() {
|
||||
import { FilterGroup } from './filter_group';
|
||||
import { SearchField } from '../search_field';
|
||||
import { SyntheticsMonitorFilterChangeHandler } from './filter_fields';
|
||||
|
||||
export function ListFilters({
|
||||
handleFilterChange,
|
||||
}: {
|
||||
handleFilterChange: SyntheticsMonitorFilterChangeHandler;
|
||||
}) {
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexGroup gutterSize="s" wrap={true}>
|
||||
<EuiFlexItem grow={2}>
|
||||
<SearchField />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={1}>
|
||||
<FilterGroup />
|
||||
<FilterGroup handleFilterChange={handleFilterChange} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
|
@ -13,18 +13,30 @@ describe('useMonitorListFilters', () => {
|
|||
it('returns expected results', () => {
|
||||
const { result } = renderHook(() => useFilters(), { wrapper: WrappedHelper });
|
||||
|
||||
expect(result.current).toStrictEqual({ locations: [], tags: [], types: [] });
|
||||
expect(result.current).toStrictEqual({
|
||||
locations: [],
|
||||
tags: [],
|
||||
monitorTypes: [],
|
||||
projects: [],
|
||||
schedules: [],
|
||||
});
|
||||
expect(defaultCore.savedObjects.client.find).toHaveBeenCalledWith({
|
||||
aggs: {
|
||||
locations: {
|
||||
terms: { field: 'synthetics-monitor.attributes.locations.id', size: 10000 },
|
||||
},
|
||||
monitorTypes: {
|
||||
terms: { field: 'synthetics-monitor.attributes.type.keyword', size: 10000 },
|
||||
},
|
||||
projects: {
|
||||
terms: { field: 'synthetics-monitor.attributes.project_id', size: 10000 },
|
||||
},
|
||||
schedules: {
|
||||
terms: { field: 'synthetics-monitor.attributes.schedule.number', size: 10000 },
|
||||
},
|
||||
tags: {
|
||||
terms: { field: 'synthetics-monitor.attributes.tags', size: 10000 },
|
||||
},
|
||||
types: {
|
||||
terms: { field: 'synthetics-monitor.attributes.type.keyword', size: 10000 },
|
||||
},
|
||||
},
|
||||
perPage: 0,
|
||||
type: 'synthetics-monitor',
|
||||
|
@ -40,18 +52,30 @@ describe('useMonitorListFilters', () => {
|
|||
{ key: 'Test 2', doc_count: 2 },
|
||||
],
|
||||
},
|
||||
tags: {
|
||||
monitorTypes: {
|
||||
buckets: [
|
||||
{ key: 'Test 3', doc_count: 3 },
|
||||
{ key: 'Test 4', doc_count: 4 },
|
||||
],
|
||||
},
|
||||
types: {
|
||||
projects: {
|
||||
buckets: [
|
||||
{ key: 'Test 5', doc_count: 5 },
|
||||
{ key: 'Test 6', doc_count: 6 },
|
||||
],
|
||||
},
|
||||
schedules: {
|
||||
buckets: [
|
||||
{ key: 'Test 7', doc_count: 7 },
|
||||
{ key: 'Test 8', doc_count: 8 },
|
||||
],
|
||||
},
|
||||
tags: {
|
||||
buckets: [
|
||||
{ key: 'Test 9', doc_count: 9 },
|
||||
{ key: 'Test 10', doc_count: 10 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -59,7 +83,13 @@ describe('useMonitorListFilters', () => {
|
|||
wrapper: WrappedHelper,
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual({ locations: [], tags: [], types: [] });
|
||||
expect(result.current).toStrictEqual({
|
||||
locations: [],
|
||||
tags: [],
|
||||
monitorTypes: [],
|
||||
projects: [],
|
||||
schedules: [],
|
||||
});
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
|
@ -68,14 +98,22 @@ describe('useMonitorListFilters', () => {
|
|||
{ label: 'Test 1', count: 1 },
|
||||
{ label: 'Test 2', count: 2 },
|
||||
],
|
||||
tags: [
|
||||
monitorTypes: [
|
||||
{ label: 'Test 3', count: 3 },
|
||||
{ label: 'Test 4', count: 4 },
|
||||
],
|
||||
types: [
|
||||
projects: [
|
||||
{ label: 'Test 5', count: 5 },
|
||||
{ label: 'Test 6', count: 6 },
|
||||
],
|
||||
schedules: [
|
||||
{ label: 'Test 7', count: 7 },
|
||||
{ label: 'Test 8', count: 8 },
|
||||
],
|
||||
tags: [
|
||||
{ label: 'Test 9', count: 9 },
|
||||
{ label: 'Test 10', count: 10 },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* 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, useEffect, useCallback, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
|
||||
import { ConfigKey } from '../../../../../../../common/runtime_types';
|
||||
import { syntheticsMonitorType } from '../../../../../../../common/types/saved_objects';
|
||||
import {
|
||||
MonitorFilterState,
|
||||
selectMonitorFiltersAndQueryState,
|
||||
setOverviewPageStateAction,
|
||||
updateManagementPageStateAction,
|
||||
} from '../../../../state';
|
||||
import { SyntheticsUrlParams } from '../../../../utils/url_params';
|
||||
import { useUrlParams } from '../../../../hooks';
|
||||
|
||||
import {
|
||||
SyntheticsMonitorFilterField,
|
||||
getMonitorFilterFields,
|
||||
getSyntheticsFilterKeyForLabel,
|
||||
SyntheticsMonitorFilterChangeHandler,
|
||||
} from './filter_fields';
|
||||
|
||||
const aggs = {
|
||||
monitorTypes: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.${ConfigKey.MONITOR_TYPE}.keyword`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.${ConfigKey.TAGS}`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
locations: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.${ConfigKey.LOCATIONS}.id`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
schedules: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.${ConfigKey.SCHEDULE}.number`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Buckets = Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
|
||||
interface AggsResponse {
|
||||
monitorTypes: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
locations: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
tags: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
projects: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
schedules: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
}
|
||||
|
||||
export const useFilters = (): Record<
|
||||
SyntheticsMonitorFilterField,
|
||||
Array<{ label: string; count: number }>
|
||||
> => {
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const { data } = useFetcher(async () => {
|
||||
return savedObjects?.client.find({
|
||||
type: syntheticsMonitorType,
|
||||
perPage: 0,
|
||||
aggs,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
const { monitorTypes, tags, locations, projects, schedules } =
|
||||
(data?.aggregations as AggsResponse) ?? {};
|
||||
return {
|
||||
monitorTypes:
|
||||
monitorTypes?.buckets?.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
tags:
|
||||
tags?.buckets?.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
locations:
|
||||
locations?.buckets?.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
projects:
|
||||
projects?.buckets
|
||||
?.filter(({ key }) => key)
|
||||
.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
schedules:
|
||||
schedules?.buckets?.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
};
|
||||
}, [data]);
|
||||
};
|
||||
|
||||
type FilterFieldWithQuery = SyntheticsMonitorFilterField | 'query';
|
||||
type FilterStateWithQuery = MonitorFilterState & { query?: string };
|
||||
|
||||
export function useMonitorFiltersState() {
|
||||
const [getUrlParams, updateUrlParams] = useUrlParams();
|
||||
const urlParams = getUrlParams();
|
||||
|
||||
const filterFieldsWithQuery: FilterFieldWithQuery[] = useMemo(() => {
|
||||
const filterFields = getMonitorFilterFields();
|
||||
return [...filterFields, 'query'];
|
||||
}, []);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const serializeFilterValue = useCallback(
|
||||
(field: FilterFieldWithQuery, selectedValues: string[] | undefined) => {
|
||||
if (field === 'query') {
|
||||
return selectedValues?.length ? selectedValues.toString() : undefined;
|
||||
}
|
||||
|
||||
return selectedValues && selectedValues.length > 0
|
||||
? JSON.stringify(
|
||||
selectedValues.map((value) => getSyntheticsFilterKeyForLabel(value, field))
|
||||
)
|
||||
: undefined;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const serializeStateValues = useCallback(
|
||||
(state: FilterStateWithQuery) => {
|
||||
return filterFieldsWithQuery.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur]: serializeFilterValue(
|
||||
cur as SyntheticsMonitorFilterField,
|
||||
state[cur as SyntheticsMonitorFilterField]
|
||||
),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
},
|
||||
[filterFieldsWithQuery, serializeFilterValue]
|
||||
);
|
||||
|
||||
const handleFilterChange: SyntheticsMonitorFilterChangeHandler = useCallback(
|
||||
(field: SyntheticsMonitorFilterField, selectedValues: string[] | undefined) => {
|
||||
// Update url to reflect the changed filter
|
||||
updateUrlParams({
|
||||
[field]: serializeFilterValue(field, selectedValues),
|
||||
});
|
||||
},
|
||||
[serializeFilterValue, updateUrlParams]
|
||||
);
|
||||
|
||||
const reduxState = useSelector(selectMonitorFiltersAndQueryState);
|
||||
const reduxStateSnapshot = JSON.stringify(serializeStateValues(reduxState));
|
||||
const urlState = filterFieldsWithQuery.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur]: urlParams[cur as keyof SyntheticsUrlParams] }),
|
||||
{}
|
||||
);
|
||||
const urlStateSerializedSnapshot = JSON.stringify(serializeStateValues(urlState));
|
||||
|
||||
const isUrlHydratedFromRedux = useRef(false);
|
||||
useEffect(() => {
|
||||
if (urlStateSerializedSnapshot !== reduxStateSnapshot) {
|
||||
if (
|
||||
urlStateSerializedSnapshot === '{}' &&
|
||||
reduxStateSnapshot !== '{}' &&
|
||||
!isUrlHydratedFromRedux.current
|
||||
) {
|
||||
// Hydrate url only during initialization
|
||||
updateUrlParams(serializeStateValues(reduxState));
|
||||
} else {
|
||||
dispatch(updateManagementPageStateAction(urlState));
|
||||
dispatch(setOverviewPageStateAction(urlState));
|
||||
}
|
||||
}
|
||||
isUrlHydratedFromRedux.current = true;
|
||||
|
||||
// Only depend on the serialized snapshot
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [urlStateSerializedSnapshot, reduxStateSnapshot]);
|
||||
|
||||
return { handleFilterChange, filterState: reduxState };
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -15,7 +15,7 @@ export function SearchField() {
|
|||
const { query } = useGetUrlParams();
|
||||
const [_, updateUrlParams] = useUrlParams();
|
||||
|
||||
const [search, setSearch] = useState(query || '');
|
||||
const [search, setSearch] = useState<undefined | string>('');
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
|
@ -27,13 +27,26 @@ export function SearchField() {
|
|||
[search]
|
||||
);
|
||||
|
||||
// Hydrate search input
|
||||
const hasInputChangedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (query && query !== search && !hasInputChangedRef.current) {
|
||||
setSearch(query);
|
||||
}
|
||||
|
||||
// Run only to sync url with input
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
|
||||
return (
|
||||
<EuiFieldSearch
|
||||
css={{ minWidth: 230 }}
|
||||
fullWidth
|
||||
placeholder={PLACEHOLDER_TEXT}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value);
|
||||
hasInputChangedRef.current = true;
|
||||
setSearch(e.target.value ?? '');
|
||||
}}
|
||||
isClearable={true}
|
||||
aria-label={PLACEHOLDER_TEXT}
|
||||
|
|
|
@ -14,8 +14,8 @@ import { WrappedHelper } from '../../../utils/testing';
|
|||
import { SyntheticsAppState } from '../../../state/root_reducer';
|
||||
import {
|
||||
selectEncryptedSyntheticsSavedMonitors,
|
||||
fetchMonitorListAction,
|
||||
MonitorListPageState,
|
||||
updateManagementPageStateAction,
|
||||
MonitorFilterState,
|
||||
} from '../../../state';
|
||||
|
||||
import { useMonitorList } from './use_monitor_list';
|
||||
|
@ -23,7 +23,8 @@ import { useMonitorList } from './use_monitor_list';
|
|||
describe('useMonitorList', () => {
|
||||
let state: SyntheticsAppState;
|
||||
let initialState: Omit<ReturnType<typeof useMonitorList>, 'loadPage' | 'reloadPage'>;
|
||||
let defaultPageState: MonitorListPageState;
|
||||
let filterState: MonitorFilterState;
|
||||
let filterStateWithQuery: MonitorFilterState & { query?: string | undefined };
|
||||
const dispatchMockFn = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -40,15 +41,12 @@ describe('useMonitorList', () => {
|
|||
pageState: state.monitorList.pageState,
|
||||
isDataQueried: false,
|
||||
syntheticsMonitors: selectEncryptedSyntheticsSavedMonitors.resultFunc(state.monitorList),
|
||||
overviewStatus: null,
|
||||
handleFilterChange: jest.fn(),
|
||||
};
|
||||
|
||||
defaultPageState = {
|
||||
...state.monitorList.pageState,
|
||||
query: '',
|
||||
locations: [],
|
||||
monitorType: [],
|
||||
tags: [],
|
||||
};
|
||||
filterState = { locations: [], monitorTypes: [], projects: [], schedules: [], tags: [] };
|
||||
filterStateWithQuery = { ...filterState, query: 'xyz' };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -60,7 +58,7 @@ describe('useMonitorList', () => {
|
|||
result: { current: hookResult },
|
||||
} = renderHook(() => useMonitorList(), { wrapper: WrappedHelper });
|
||||
|
||||
expect(hookResult).toMatchObject(initialState);
|
||||
expect(hookResult).toMatchObject({ ...initialState, handleFilterChange: expect.any(Function) });
|
||||
});
|
||||
|
||||
it('dispatches correct action for query url param', async () => {
|
||||
|
@ -79,18 +77,26 @@ describe('useMonitorList', () => {
|
|||
renderHook(() => useMonitorList(), { wrapper: WrapperWithState });
|
||||
|
||||
expect(dispatchMockFn).toHaveBeenCalledWith(
|
||||
fetchMonitorListAction.get({ ...defaultPageState, query })
|
||||
updateManagementPageStateAction(filterStateWithQuery)
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches correct action for filter url param', async () => {
|
||||
const tags = ['abc', 'xyz'];
|
||||
const locations = ['loc1', 'loc1'];
|
||||
const monitorType = ['browser'];
|
||||
const exp = {
|
||||
...filterStateWithQuery,
|
||||
tags: ['abc', 'xyz'],
|
||||
locations: ['loc1', 'loc1'],
|
||||
monitorTypes: ['browser'],
|
||||
schedules: ['browser'],
|
||||
projects: ['proj-1'],
|
||||
query: '',
|
||||
};
|
||||
|
||||
const url = `/monitor/1?tags=${JSON.stringify(tags)}&locations=${JSON.stringify(
|
||||
locations
|
||||
)}&monitorType=${JSON.stringify(monitorType)}`;
|
||||
const url = `/monitor/1?tags=${JSON.stringify(exp.tags)}&locations=${JSON.stringify(
|
||||
exp.locations
|
||||
)}&monitorTypes=${JSON.stringify(exp.monitorTypes)}&schedules=${JSON.stringify(
|
||||
exp.schedules
|
||||
)}&projects=${JSON.stringify(exp.projects)}`;
|
||||
|
||||
jest.useFakeTimers().setSystemTime(Date.now());
|
||||
const WrapperWithState = ({ children }: { children: React.ReactElement }) => {
|
||||
|
@ -103,8 +109,6 @@ describe('useMonitorList', () => {
|
|||
|
||||
renderHook(() => useMonitorList(), { wrapper: WrapperWithState });
|
||||
|
||||
expect(dispatchMockFn).toHaveBeenCalledWith(
|
||||
fetchMonitorListAction.get({ ...defaultPageState, tags, locations, monitorType })
|
||||
);
|
||||
expect(dispatchMockFn).toHaveBeenCalledWith(updateManagementPageStateAction(exp));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,16 +5,21 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback, useRef } from 'react';
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useGetUrlParams } from '../../../hooks';
|
||||
import { useDebounce } from 'react-use';
|
||||
import {
|
||||
fetchMonitorListAction,
|
||||
quietFetchMonitorListAction,
|
||||
fetchOverviewStatusAction,
|
||||
MonitorListPageState,
|
||||
selectEncryptedSyntheticsSavedMonitors,
|
||||
selectMonitorListState,
|
||||
selectOverviewStatus,
|
||||
updateManagementPageStateAction,
|
||||
} from '../../../state';
|
||||
import { useSyntheticsRefreshContext } from '../../../contexts';
|
||||
import { useMonitorFiltersState } from '../common/monitor_filters/use_filters';
|
||||
|
||||
export function useMonitorList() {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -22,43 +27,44 @@ export function useMonitorList() {
|
|||
|
||||
const { pageState, loading, loaded, error, data } = useSelector(selectMonitorListState);
|
||||
const syntheticsMonitors = useSelector(selectEncryptedSyntheticsSavedMonitors);
|
||||
const { status: overviewStatus } = useSelector(selectOverviewStatus);
|
||||
|
||||
const { query, tags, monitorType, locations: locationFilters } = useGetUrlParams();
|
||||
|
||||
const { search } = useLocation();
|
||||
const { handleFilterChange } = useMonitorFiltersState();
|
||||
const { refreshInterval } = useSyntheticsRefreshContext();
|
||||
|
||||
const loadPage = useCallback(
|
||||
(state: MonitorListPageState) =>
|
||||
dispatch(
|
||||
fetchMonitorListAction.get({
|
||||
...state,
|
||||
query,
|
||||
tags,
|
||||
monitorType,
|
||||
locations: locationFilters,
|
||||
})
|
||||
),
|
||||
[dispatch, locationFilters, monitorType, query, tags]
|
||||
(state: MonitorListPageState) => {
|
||||
dispatch(updateManagementPageStateAction(state));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const reloadPage = useCallback(() => loadPage(pageState), [pageState, loadPage]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadPage();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [search]);
|
||||
const reloadPageQuiet = useCallback(() => {
|
||||
dispatch(quietFetchMonitorListAction(pageState));
|
||||
}, [dispatch, pageState]);
|
||||
|
||||
// Initial loading
|
||||
// Periodically refresh
|
||||
useEffect(() => {
|
||||
if (!loading && !isDataQueriedRef.current) {
|
||||
isDataQueriedRef.current = true;
|
||||
reloadPage();
|
||||
}
|
||||
const intervalId = setInterval(() => {
|
||||
reloadPageQuiet();
|
||||
}, refreshInterval);
|
||||
|
||||
if (loading) {
|
||||
isDataQueriedRef.current = true;
|
||||
}
|
||||
}, [reloadPage, syntheticsMonitors, loading]);
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [reloadPageQuiet, refreshInterval]);
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
const overviewStatusArgs = { ...pageState, perPage: pageState.pageSize };
|
||||
|
||||
dispatch(fetchOverviewStatusAction.get(overviewStatusArgs));
|
||||
dispatch(fetchMonitorListAction.get(pageState));
|
||||
},
|
||||
500,
|
||||
[pageState]
|
||||
);
|
||||
|
||||
return {
|
||||
loading,
|
||||
|
@ -71,5 +77,7 @@ export function useMonitorList() {
|
|||
reloadPage,
|
||||
isDataQueried: isDataQueriedRef.current,
|
||||
absoluteTotal: data.absoluteTotal ?? 0,
|
||||
overviewStatus,
|
||||
handleFilterChange,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { FieldValueSelection } from '@kbn/observability-plugin/public';
|
||||
import { FilterItem } from './filter_group';
|
||||
import { useGetUrlParams, useUrlParams } from '../../../../hooks';
|
||||
|
||||
export const FilterButton = ({ filter }: { filter: FilterItem }) => {
|
||||
const { label, values, field } = filter;
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const [, updateUrlParams] = useUrlParams();
|
||||
|
||||
const urlParams = useGetUrlParams();
|
||||
|
||||
return (
|
||||
<FieldValueSelection
|
||||
selectedValue={urlParams[field] || []}
|
||||
singleSelection={false}
|
||||
label={label}
|
||||
values={
|
||||
query
|
||||
? values.filter(({ label: str }) => str.toLowerCase().includes(query.toLowerCase()))
|
||||
: values
|
||||
}
|
||||
setQuery={setQuery}
|
||||
onChange={(selectedValues) => {
|
||||
updateUrlParams({
|
||||
[field]:
|
||||
selectedValues && selectedValues.length > 0
|
||||
? JSON.stringify(selectedValues)
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
allowExclusions={false}
|
||||
loading={false}
|
||||
asFilterButton={true}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -1,82 +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 { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { useFetcher } from '@kbn/observability-plugin/public';
|
||||
import { useMemo } from 'react';
|
||||
import { syntheticsMonitorType } from '../../../../../../../common/types/saved_objects';
|
||||
|
||||
const aggs = {
|
||||
types: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.type.keyword`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.tags`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
locations: {
|
||||
terms: {
|
||||
field: `${syntheticsMonitorType}.attributes.locations.id`,
|
||||
size: 10000,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Buckets = Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
|
||||
interface AggsResponse {
|
||||
types: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
locations: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
tags: {
|
||||
buckets: Buckets;
|
||||
};
|
||||
}
|
||||
|
||||
export const useFilters = () => {
|
||||
const { savedObjects } = useKibana().services;
|
||||
|
||||
const { data } = useFetcher(async () => {
|
||||
return savedObjects?.client.find({
|
||||
type: syntheticsMonitorType,
|
||||
perPage: 0,
|
||||
aggs,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return useMemo(() => {
|
||||
const { types, tags, locations } = (data?.aggregations as AggsResponse) ?? {};
|
||||
return {
|
||||
types:
|
||||
types?.buckets?.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
tags:
|
||||
tags?.buckets?.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
locations:
|
||||
locations?.buckets?.map(({ key, doc_count: count }) => ({
|
||||
label: key,
|
||||
count,
|
||||
})) ?? [],
|
||||
};
|
||||
}, [data]);
|
||||
};
|
|
@ -5,13 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
|
||||
import type { useMonitorList } from '../hooks/use_monitor_list';
|
||||
import { MonitorAsyncError } from './monitor_errors/monitor_async_error';
|
||||
import { useOverviewStatus } from '../hooks/use_overview_status';
|
||||
import { ListFilters } from './list_filters/list_filters';
|
||||
import { ListFilters } from '../common/monitor_filters/list_filters';
|
||||
import { MonitorList } from './monitor_list_table/monitor_list';
|
||||
import { MonitorStats } from './monitor_stats/monitor_stats';
|
||||
|
||||
|
@ -31,6 +30,8 @@ export const MonitorListContainer = ({
|
|||
absoluteTotal,
|
||||
loadPage,
|
||||
reloadPage,
|
||||
overviewStatus,
|
||||
handleFilterChange,
|
||||
} = monitorListProps;
|
||||
|
||||
// TODO: Display inline errors in the management table
|
||||
|
@ -41,18 +42,6 @@ export const MonitorListContainer = ({
|
|||
// sortOrder: pageState.sortOrder,
|
||||
// });
|
||||
|
||||
const overviewStatusArgs = useMemo(() => {
|
||||
return {
|
||||
pageState: { ...pageState, perPage: pageState.pageSize },
|
||||
};
|
||||
}, [pageState]);
|
||||
|
||||
const { status, reload: reloadStatus } = useOverviewStatus(overviewStatusArgs);
|
||||
|
||||
useEffect(() => {
|
||||
reloadStatus();
|
||||
}, [reloadStatus, syntheticsMonitors]);
|
||||
|
||||
if (!isEnabled && absoluteTotal === 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -60,9 +49,9 @@ export const MonitorListContainer = ({
|
|||
return (
|
||||
<>
|
||||
<MonitorAsyncError />
|
||||
<ListFilters />
|
||||
<ListFilters handleFilterChange={handleFilterChange} />
|
||||
<EuiSpacer />
|
||||
<MonitorStats status={status} />
|
||||
<MonitorStats overviewStatus={overviewStatus} />
|
||||
<EuiSpacer />
|
||||
<MonitorList
|
||||
syntheticsMonitors={syntheticsMonitors}
|
||||
|
@ -72,7 +61,7 @@ export const MonitorListContainer = ({
|
|||
loading={monitorsLoading}
|
||||
loadPage={loadPage}
|
||||
reloadPage={reloadPage}
|
||||
status={status}
|
||||
overviewStatus={overviewStatus}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -34,15 +34,13 @@ import { MonitorLocations } from './monitor_locations';
|
|||
|
||||
export function useMonitorListColumns({
|
||||
canEditSynthetics,
|
||||
reloadPage,
|
||||
loading,
|
||||
status,
|
||||
overviewStatus,
|
||||
setMonitorPendingDeletion,
|
||||
}: {
|
||||
canEditSynthetics: boolean;
|
||||
loading: boolean;
|
||||
status: OverviewStatusState | null;
|
||||
reloadPage: () => void;
|
||||
overviewStatus: OverviewStatusState | null;
|
||||
setMonitorPendingDeletion: (config: EncryptedSyntheticsSavedMonitor) => void;
|
||||
}): Array<EuiBasicTableColumn<EncryptedSyntheticsSavedMonitor>> {
|
||||
const history = useHistory();
|
||||
|
@ -66,7 +64,7 @@ export function useMonitorListColumns({
|
|||
),
|
||||
},
|
||||
// Only show Project ID column if project monitors are present
|
||||
...(status?.projectMonitorsCount ?? 0 > 0
|
||||
...(overviewStatus?.projectMonitorsCount ?? 0 > 0
|
||||
? [
|
||||
{
|
||||
align: 'left' as const,
|
||||
|
@ -91,7 +89,7 @@ export function useMonitorListColumns({
|
|||
ariaLabel={labels.getFilterForTypeMessage(monitor[ConfigKey.MONITOR_TYPE])}
|
||||
onClick={() => {
|
||||
history.push({
|
||||
search: `monitorType=${encodeURIComponent(
|
||||
search: `monitorTypes=${encodeURIComponent(
|
||||
JSON.stringify([monitor[ConfigKey.MONITOR_TYPE]])
|
||||
)}`,
|
||||
});
|
||||
|
@ -119,7 +117,7 @@ export function useMonitorListColumns({
|
|||
<MonitorLocations
|
||||
monitorId={monitor[ConfigKey.CONFIG_ID] ?? monitor.id}
|
||||
locations={locations}
|
||||
status={status}
|
||||
status={overviewStatus}
|
||||
/>
|
||||
) : null,
|
||||
},
|
||||
|
@ -149,7 +147,7 @@ export function useMonitorListColumns({
|
|||
<MonitorEnabled
|
||||
configId={monitor[ConfigKey.CONFIG_ID]}
|
||||
monitor={monitor}
|
||||
reloadPage={reloadPage}
|
||||
reloadPage={() => {}}
|
||||
isSwitchable={!loading}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -37,7 +37,7 @@ interface Props {
|
|||
loading: boolean;
|
||||
loadPage: (state: MonitorListPageState) => void;
|
||||
reloadPage: () => void;
|
||||
status: OverviewStatusState | null;
|
||||
overviewStatus: OverviewStatusState | null;
|
||||
}
|
||||
|
||||
export const MonitorList = ({
|
||||
|
@ -46,7 +46,7 @@ export const MonitorList = ({
|
|||
total,
|
||||
error,
|
||||
loading,
|
||||
status,
|
||||
overviewStatus,
|
||||
loadPage,
|
||||
reloadPage,
|
||||
}: Props) => {
|
||||
|
@ -98,8 +98,7 @@ export const MonitorList = ({
|
|||
const columns = useMonitorListColumns({
|
||||
canEditSynthetics,
|
||||
loading,
|
||||
reloadPage,
|
||||
status,
|
||||
overviewStatus,
|
||||
setMonitorPendingDeletion,
|
||||
});
|
||||
|
||||
|
|
|
@ -24,7 +24,11 @@ import * as labels from '../labels';
|
|||
import { MonitorTestRunsCount } from './monitor_test_runs';
|
||||
import { MonitorTestRunsSparkline } from './monitor_test_runs_sparkline';
|
||||
|
||||
export const MonitorStats = ({ status }: { status: OverviewStatusState | null }) => {
|
||||
export const MonitorStats = ({
|
||||
overviewStatus,
|
||||
}: {
|
||||
overviewStatus: OverviewStatusState | null;
|
||||
}) => {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
return (
|
||||
|
@ -43,11 +47,11 @@ export const MonitorStats = ({ status }: { status: OverviewStatusState | null })
|
|||
<EuiFlexItem css={{ display: 'flex', flexDirection: 'row', gap: euiTheme.size.l }}>
|
||||
<MonitorStat
|
||||
description={labels.CONFIGURATIONS_LABEL}
|
||||
value={status?.allMonitorsCount}
|
||||
value={overviewStatus?.allMonitorsCount}
|
||||
/>
|
||||
<MonitorStat
|
||||
description={labels.DISABLED_LABEL}
|
||||
value={status?.disabledMonitorsCount}
|
||||
value={overviewStatus?.disabledMonitorsCount}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiPanel>
|
||||
|
@ -64,9 +68,9 @@ export const MonitorStats = ({ status }: { status: OverviewStatusState | null })
|
|||
<EuiFlexItem
|
||||
css={{ display: 'flex', flexDirection: 'row', gap: euiTheme.size.l, height: '200px' }}
|
||||
>
|
||||
<MonitorTestRunsCount />
|
||||
<MonitorTestRunsCount monitorIds={overviewStatus?.allIds ?? []} />
|
||||
<EuiFlexItem grow={true}>
|
||||
<MonitorTestRunsSparkline />
|
||||
<MonitorTestRunsSparkline monitorIds={overviewStatus?.allIds ?? []} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -15,7 +15,7 @@ import { useAbsoluteDate } from '../../../../hooks';
|
|||
import { ClientPluginsStart } from '../../../../../../plugin';
|
||||
import * as labels from '../labels';
|
||||
|
||||
export const MonitorTestRunsCount = () => {
|
||||
export const MonitorTestRunsCount = ({ monitorIds }: { monitorIds: string[] }) => {
|
||||
const { observability } = useKibana<ClientPluginsStart>().services;
|
||||
const theme = useTheme();
|
||||
|
||||
|
@ -31,11 +31,11 @@ export const MonitorTestRunsCount = () => {
|
|||
{
|
||||
time: { from: absFrom, to: absTo },
|
||||
reportDefinitions: {
|
||||
'monitor.id': [],
|
||||
'observer.geo.name': [],
|
||||
'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'], // Show no data when monitorIds is empty
|
||||
},
|
||||
dataType: 'synthetics',
|
||||
selectedMetricField: 'monitor_total_runs',
|
||||
filters: [],
|
||||
name: labels.TEST_RUNS_LABEL,
|
||||
color: theme.eui.euiColorVis1,
|
||||
},
|
||||
|
|
|
@ -14,7 +14,7 @@ import { useAbsoluteDate } from '../../../../hooks';
|
|||
import { ClientPluginsStart } from '../../../../../../plugin';
|
||||
import * as labels from '../labels';
|
||||
|
||||
export const MonitorTestRunsSparkline = () => {
|
||||
export const MonitorTestRunsSparkline = ({ monitorIds }: { monitorIds: string[] }) => {
|
||||
const { observability } = useKibana<ClientPluginsStart>().services;
|
||||
|
||||
const { ExploratoryViewEmbeddable } = observability;
|
||||
|
@ -34,11 +34,11 @@ export const MonitorTestRunsSparkline = () => {
|
|||
seriesType: 'area',
|
||||
time: { from, to },
|
||||
reportDefinitions: {
|
||||
'monitor.id': [],
|
||||
'observer.geo.name': [],
|
||||
'monitor.id': monitorIds.length > 0 ? monitorIds : ['false-monitor-id'], // Show no data when monitorIds is empty
|
||||
},
|
||||
dataType: 'synthetics',
|
||||
selectedMetricField: 'monitor.check_group',
|
||||
filters: [],
|
||||
name: labels.TEST_RUNS_LABEL,
|
||||
color: theme.eui.euiColorVis1,
|
||||
operationType: 'unique_count',
|
||||
|
|
|
@ -30,6 +30,13 @@ const MonitorPage: React.FC = () => {
|
|||
|
||||
useMonitorListBreadcrumbs();
|
||||
|
||||
const {
|
||||
error: enablementError,
|
||||
enablement: { isEnabled, canEnable },
|
||||
loading: enablementLoading,
|
||||
enableSynthetics,
|
||||
} = useEnablement();
|
||||
|
||||
const monitorListProps = useMonitorList();
|
||||
const {
|
||||
syntheticsMonitors,
|
||||
|
@ -38,13 +45,6 @@ const MonitorPage: React.FC = () => {
|
|||
absoluteTotal,
|
||||
} = monitorListProps;
|
||||
|
||||
const {
|
||||
error: enablementError,
|
||||
enablement: { isEnabled, canEnable },
|
||||
loading: enablementLoading,
|
||||
enableSynthetics,
|
||||
} = useEnablement();
|
||||
|
||||
const { loading: locationsLoading } = useLocations();
|
||||
const showEmptyState = isEnabled !== undefined && syntheticsMonitors.length === 0;
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@ import { EuiFlexGroup, EuiSpacer, EuiFlexItem } from '@elastic/eui';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTrackPageview } from '@kbn/observability-plugin/public';
|
||||
import { Redirect, useLocation } from 'react-router-dom';
|
||||
import { FilterGroup } from '../common/monitor_filters/filter_group';
|
||||
import { OverviewAlerts } from './overview/overview_alerts';
|
||||
import { useEnablement, useGetUrlParams } from '../../../hooks';
|
||||
import { useEnablement } from '../../../hooks';
|
||||
import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context';
|
||||
import {
|
||||
fetchMonitorOverviewAction,
|
||||
quietFetchOverviewAction,
|
||||
setOverviewPageStateAction,
|
||||
selectOverviewPageState,
|
||||
selectServiceLocationsState,
|
||||
} from '../../../state';
|
||||
|
@ -40,7 +40,6 @@ export const OverviewPage: React.FC = () => {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const { lastRefresh } = useSyntheticsRefreshContext();
|
||||
const { query } = useGetUrlParams();
|
||||
const { search } = useLocation();
|
||||
|
||||
const pageState = useSelector(selectOverviewPageState);
|
||||
|
@ -52,14 +51,6 @@ export const OverviewPage: React.FC = () => {
|
|||
}
|
||||
}, [dispatch, locationsLoaded, locationsLoading]);
|
||||
|
||||
// fetch overview for query state changes
|
||||
useEffect(() => {
|
||||
if (pageState.query !== query) {
|
||||
dispatch(fetchMonitorOverviewAction.get({ ...pageState, query }));
|
||||
dispatch(setOverviewPageStateAction({ query }));
|
||||
}
|
||||
}, [dispatch, pageState, query]);
|
||||
|
||||
// fetch overview for all other page state changes
|
||||
useEffect(() => {
|
||||
dispatch(fetchMonitorOverviewAction.get(pageState));
|
||||
|
@ -75,13 +66,19 @@ export const OverviewPage: React.FC = () => {
|
|||
loading: enablementLoading,
|
||||
} = useEnablement();
|
||||
|
||||
const { syntheticsMonitors, loading: monitorsLoading, loaded: monitorsLoaded } = useMonitorList();
|
||||
const {
|
||||
syntheticsMonitors,
|
||||
loading: monitorsLoading,
|
||||
loaded: monitorsLoaded,
|
||||
handleFilterChange,
|
||||
} = useMonitorList();
|
||||
|
||||
if (
|
||||
!search &&
|
||||
!enablementLoading &&
|
||||
isEnabled &&
|
||||
!monitorsLoading &&
|
||||
monitorsLoaded &&
|
||||
syntheticsMonitors.length === 0
|
||||
) {
|
||||
return <Redirect to={GETTING_STARTED_ROUTE} />;
|
||||
|
@ -99,13 +96,16 @@ export const OverviewPage: React.FC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexGroup gutterSize="s" wrap={true}>
|
||||
<EuiFlexItem>
|
||||
<SearchField />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<QuickFilters />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FilterGroup handleFilterChange={handleFilterChange} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
{Boolean(!monitorsLoaded || syntheticsMonitors?.length > 0) && (
|
||||
|
|
|
@ -5,16 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ErrorToastOptions } from '@kbn/core-notifications-browser';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { UpsertMonitorResponse } from '..';
|
||||
import {
|
||||
EncryptedSyntheticsMonitor,
|
||||
MonitorManagementListResult,
|
||||
SyntheticsMonitor,
|
||||
} from '../../../../../common/runtime_types';
|
||||
import { UpsertMonitorError, UpsertMonitorRequest, UpsertMonitorResponse } from '..';
|
||||
import { MonitorManagementListResult } from '../../../../../common/runtime_types';
|
||||
import { createAsyncAction } from '../utils/actions';
|
||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
|
||||
import { MonitorListPageState } from './models';
|
||||
|
||||
|
@ -22,29 +16,9 @@ export const fetchMonitorListAction = createAsyncAction<
|
|||
MonitorListPageState,
|
||||
MonitorManagementListResult
|
||||
>('fetchMonitorListAction');
|
||||
|
||||
interface ToastParams<MessageType> {
|
||||
message: MessageType;
|
||||
lifetimeMs: number;
|
||||
testAttribute?: string;
|
||||
}
|
||||
|
||||
export interface UpsertMonitorRequest {
|
||||
configId: string;
|
||||
monitor: Partial<SyntheticsMonitor> | Partial<EncryptedSyntheticsMonitor>;
|
||||
success: ToastParams<string>;
|
||||
error: ToastParams<ErrorToastOptions>;
|
||||
/**
|
||||
* The effect will perform a quiet refresh of the overview state
|
||||
* after a successful upsert. The default behavior is to perform the fetch.
|
||||
*/
|
||||
shouldQuietFetchAfterSuccess?: boolean;
|
||||
}
|
||||
|
||||
interface UpsertMonitorError {
|
||||
configId: string;
|
||||
error: IHttpSerializedFetchError;
|
||||
}
|
||||
export const quietFetchMonitorListAction = createAction<MonitorListPageState>(
|
||||
'quietFetchMonitorListAction'
|
||||
);
|
||||
|
||||
export const fetchUpsertMonitorAction = createAction<UpsertMonitorRequest>('fetchUpsertMonitor');
|
||||
export const fetchUpsertSuccessAction = createAction<{
|
||||
|
@ -62,3 +36,7 @@ export const enableMonitorAlertAction = createAsyncAction<
|
|||
>('enableMonitorAlertAction');
|
||||
|
||||
export const clearMonitorUpsertStatus = createAction<string>('clearMonitorUpsertStatus');
|
||||
|
||||
export const updateManagementPageStateAction = createAction<Partial<MonitorListPageState>>(
|
||||
'updateManagementPageState'
|
||||
);
|
||||
|
|
|
@ -31,7 +31,9 @@ function toMonitorManagementListQueryArgs(
|
|||
query: pageState.query,
|
||||
tags: pageState.tags,
|
||||
locations: pageState.locations,
|
||||
monitorType: pageState.monitorType,
|
||||
monitorTypes: pageState.monitorTypes,
|
||||
projects: pageState.projects,
|
||||
schedules: pageState.schedules,
|
||||
searchFields: [],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
*/
|
||||
|
||||
import { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { call, put, takeEvery, takeLeading, select } from 'redux-saga/effects';
|
||||
import { call, put, takeEvery, select, debounce } from 'redux-saga/effects';
|
||||
import { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import { enableDefaultAlertingAction } from '../alert_rules';
|
||||
import { ConfigKey, SyntheticsMonitor } from '../../../../../common/runtime_types';
|
||||
import { kibanaService } from '../../../../utils/kibana_service';
|
||||
import { MonitorOverviewPageState, quietFetchOverviewStatusAction } from '../overview';
|
||||
import { quietFetchOverviewAction } from '../overview/actions';
|
||||
|
@ -22,15 +23,16 @@ import {
|
|||
fetchUpsertFailureAction,
|
||||
fetchUpsertMonitorAction,
|
||||
fetchUpsertSuccessAction,
|
||||
UpsertMonitorRequest,
|
||||
quietFetchMonitorListAction,
|
||||
} from './actions';
|
||||
import { fetchMonitorManagementList, fetchUpsertMonitor } from './api';
|
||||
import { toastTitle } from './toast_title';
|
||||
import { ConfigKey, SyntheticsMonitor } from '../../../../../common/runtime_types';
|
||||
import { UpsertMonitorRequest } from './models';
|
||||
|
||||
export function* fetchMonitorListEffect() {
|
||||
yield takeLeading(
|
||||
fetchMonitorListAction.get,
|
||||
yield debounce(
|
||||
300, // Only take the latest while ignoring any intermediate triggers
|
||||
[fetchMonitorListAction.get, quietFetchMonitorListAction],
|
||||
fetchEffectFactory(
|
||||
fetchMonitorManagementList,
|
||||
fetchMonitorListAction.success,
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { FETCH_STATUS } from '@kbn/observability-plugin/public';
|
||||
|
||||
|
@ -26,6 +25,7 @@ import {
|
|||
fetchUpsertFailureAction,
|
||||
fetchUpsertMonitorAction,
|
||||
fetchUpsertSuccessAction,
|
||||
updateManagementPageStateAction,
|
||||
} from './actions';
|
||||
|
||||
export interface MonitorListState {
|
||||
|
@ -56,10 +56,10 @@ const initialState: MonitorListState = {
|
|||
|
||||
export const monitorListReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(fetchMonitorListAction.get, (state, action) => {
|
||||
if (!isEqual(state.pageState, action.payload)) {
|
||||
state.pageState = action.payload;
|
||||
}
|
||||
.addCase(updateManagementPageStateAction, (state, action) => {
|
||||
state.pageState = { ...state.pageState, ...action.payload };
|
||||
})
|
||||
.addCase(fetchMonitorListAction.get, (state) => {
|
||||
state.loading = true;
|
||||
state.loaded = false;
|
||||
})
|
||||
|
|
|
@ -5,20 +5,54 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ErrorToastOptions } from '@kbn/core-notifications-browser';
|
||||
|
||||
import {
|
||||
EncryptedSyntheticsMonitor,
|
||||
EncryptedSyntheticsSavedMonitor,
|
||||
FetchMonitorManagementListQueryArgs,
|
||||
SyntheticsMonitor,
|
||||
} from '../../../../../common/runtime_types';
|
||||
|
||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
|
||||
export type MonitorListSortField = `${keyof EncryptedSyntheticsSavedMonitor}.keyword` | 'enabled';
|
||||
|
||||
export interface MonitorListPageState {
|
||||
export interface MonitorFilterState {
|
||||
tags?: string[];
|
||||
monitorTypes?: string[];
|
||||
projects?: string[];
|
||||
schedules?: string[];
|
||||
locations?: string[];
|
||||
}
|
||||
|
||||
export interface MonitorListPageState extends MonitorFilterState {
|
||||
query?: string;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
sortField: MonitorListSortField;
|
||||
sortOrder: NonNullable<FetchMonitorManagementListQueryArgs['sortOrder']>;
|
||||
tags?: string[];
|
||||
monitorType?: string[];
|
||||
locations?: string[];
|
||||
}
|
||||
|
||||
interface ToastParams<MessageType> {
|
||||
message: MessageType;
|
||||
lifetimeMs: number;
|
||||
testAttribute?: string;
|
||||
}
|
||||
|
||||
export interface UpsertMonitorRequest {
|
||||
configId: string;
|
||||
monitor: Partial<SyntheticsMonitor> | Partial<EncryptedSyntheticsMonitor>;
|
||||
success: ToastParams<string>;
|
||||
error: ToastParams<ErrorToastOptions>;
|
||||
/**
|
||||
* The effect will perform a quiet refresh of the overview state
|
||||
* after a successful upsert. The default behavior is to perform the fetch.
|
||||
*/
|
||||
shouldQuietFetchAfterSuccess?: boolean;
|
||||
}
|
||||
|
||||
export interface UpsertMonitorError {
|
||||
configId: string;
|
||||
error: IHttpSerializedFetchError;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import { ConfigKey, EncryptedSyntheticsSavedMonitor } from '../../../../../common/runtime_types';
|
||||
import { SyntheticsAppState } from '../root_reducer';
|
||||
import { MonitorFilterState } from './models';
|
||||
|
||||
export const selectMonitorListState = (state: SyntheticsAppState) => state.monitorList;
|
||||
export const selectEncryptedSyntheticsSavedMonitors = createSelector(
|
||||
|
@ -21,6 +22,15 @@ export const selectEncryptedSyntheticsSavedMonitors = createSelector(
|
|||
created_at: monitor.created_at,
|
||||
})) as EncryptedSyntheticsSavedMonitor[]
|
||||
);
|
||||
|
||||
export const selectMonitorFiltersAndQueryState = createSelector(selectMonitorListState, (state) => {
|
||||
const { monitorTypes, tags, locations, projects, schedules }: MonitorFilterState =
|
||||
state.pageState;
|
||||
const { query } = state.pageState;
|
||||
|
||||
return { monitorTypes, tags, locations, projects, schedules, query };
|
||||
});
|
||||
|
||||
export const selectMonitorUpsertStatuses = (state: SyntheticsAppState) =>
|
||||
state.monitorList.monitorUpsertStatuses;
|
||||
|
||||
|
|
|
@ -32,7 +32,9 @@ function toMonitorOverviewQueryArgs(
|
|||
query: pageState.query,
|
||||
tags: pageState.tags,
|
||||
locations: pageState.locations,
|
||||
monitorType: pageState.monitorType,
|
||||
projects: pageState.projects,
|
||||
schedules: pageState.schedules,
|
||||
monitorTypes: pageState.monitorTypes,
|
||||
sortField: pageState.sortField,
|
||||
sortOrder: pageState.sortOrder,
|
||||
searchFields: [],
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { takeLatest, takeLeading } from 'redux-saga/effects';
|
||||
import { takeLatest, debounce } from 'redux-saga/effects';
|
||||
import { fetchEffectFactory } from '../utils/fetch_effect';
|
||||
import {
|
||||
fetchMonitorOverviewAction,
|
||||
|
@ -16,7 +16,8 @@ import {
|
|||
import { fetchMonitorOverview, fetchOverviewStatus } from './api';
|
||||
|
||||
export function* fetchMonitorOverviewEffect() {
|
||||
yield takeLeading(
|
||||
yield debounce(
|
||||
300, // Only take the latest while ignoring any intermediate triggers
|
||||
[fetchMonitorOverviewAction.get, quietFetchOverviewAction.get],
|
||||
fetchEffectFactory(
|
||||
fetchMonitorOverview,
|
||||
|
|
|
@ -7,13 +7,11 @@
|
|||
import { MonitorOverviewResult, OverviewStatusState } from '../../../../../common/runtime_types';
|
||||
|
||||
import { IHttpSerializedFetchError } from '../utils/http_error';
|
||||
import { MonitorFilterState } from '../monitor_list';
|
||||
|
||||
export interface MonitorOverviewPageState {
|
||||
export interface MonitorOverviewPageState extends MonitorFilterState {
|
||||
perPage: number;
|
||||
query?: string;
|
||||
tags?: string[];
|
||||
monitorType?: string[];
|
||||
locations?: string[];
|
||||
sortOrder: 'asc' | 'desc';
|
||||
sortField: string;
|
||||
}
|
||||
|
|
|
@ -67,7 +67,8 @@ export const mockState: SyntheticsAppState = {
|
|||
sortField: `${ConfigKey.NAME}.keyword`,
|
||||
sortOrder: 'asc',
|
||||
tags: undefined,
|
||||
monitorType: undefined,
|
||||
monitorTypes: undefined,
|
||||
projects: undefined,
|
||||
locations: undefined,
|
||||
},
|
||||
monitorUpsertStatuses: {},
|
||||
|
|
|
@ -71,7 +71,9 @@ describe('getSupportedUrlParams', () => {
|
|||
statusFilter: STATUS_FILTER,
|
||||
query: '',
|
||||
locations: [],
|
||||
monitorType: [],
|
||||
monitorTypes: [],
|
||||
projects: [],
|
||||
schedules: [],
|
||||
tags: [],
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,9 +28,11 @@ export interface SyntheticsUrlParams {
|
|||
query?: string;
|
||||
tags?: string[];
|
||||
locations?: string[];
|
||||
monitorType?: string[];
|
||||
monitorTypes?: string[];
|
||||
status?: string[];
|
||||
locationId?: string;
|
||||
projects?: string[];
|
||||
schedules?: string[];
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -87,9 +89,11 @@ export const getSupportedUrlParams = (params: {
|
|||
focusConnectorField,
|
||||
query,
|
||||
tags,
|
||||
monitorType,
|
||||
monitorTypes,
|
||||
locations,
|
||||
locationId,
|
||||
projects,
|
||||
schedules,
|
||||
} = filteredParams;
|
||||
|
||||
return {
|
||||
|
@ -114,8 +118,10 @@ export const getSupportedUrlParams = (params: {
|
|||
focusConnectorField: !!focusConnectorField,
|
||||
query: query || '',
|
||||
tags: tags ? JSON.parse(tags) : [],
|
||||
monitorType: monitorType ? JSON.parse(monitorType) : [],
|
||||
monitorTypes: monitorTypes ? JSON.parse(monitorTypes) : [],
|
||||
locations: locations ? JSON.parse(locations) : [],
|
||||
projects: projects ? JSON.parse(projects) : [],
|
||||
schedules: schedules ? JSON.parse(schedules) : [],
|
||||
locationId: locationId || undefined,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -153,6 +153,7 @@ export class StatusRuleExecutor {
|
|||
allMonitorsCount: allIds.length,
|
||||
disabledMonitorsCount: allIds.length,
|
||||
projectMonitorsCount,
|
||||
allIds,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { intersection } from 'lodash';
|
||||
import { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { SUMMARY_FILTER } from '../../common/constants/client_defaults';
|
||||
import { UptimeEsClient } from '../legacy_uptime/lib/lib';
|
||||
|
@ -117,7 +118,6 @@ export async function queryMonitorStatus(
|
|||
for await (const response of promises) {
|
||||
response.body.aggregations?.id.buckets.forEach(
|
||||
({ location, key: queryId }: { location: any; key: string }) => {
|
||||
const monLocations = monitorLocationsMap?.[queryId];
|
||||
const locationSummaries = location.buckets.map(
|
||||
({ status, key: locationName }: { key: string; status: any }) => {
|
||||
const ping = status.hits.hits[0]._source as Ping & { '@timestamp': string };
|
||||
|
@ -125,8 +125,11 @@ export async function queryMonitorStatus(
|
|||
}
|
||||
) as Array<{ location: string; ping: Ping & { '@timestamp': string } }>;
|
||||
|
||||
// discard any locations that are not in the monitorLocationsMap for the given monitor
|
||||
monLocations?.forEach((monLocation) => {
|
||||
// discard any locations that are not in the monitorLocationsMap for the given monitor as well as those which are
|
||||
// in monitorLocationsMap but not in listOfLocations
|
||||
const monLocations = monitorLocationsMap?.[queryId];
|
||||
const monQueriedLocations = intersection(monLocations, listOfLocations);
|
||||
monQueriedLocations?.forEach((monLocation) => {
|
||||
const locationSummary = locationSummaries.find(
|
||||
(summary) => summary.location === monLocation
|
||||
);
|
||||
|
@ -166,5 +169,5 @@ export async function queryMonitorStatus(
|
|||
}
|
||||
);
|
||||
}
|
||||
return { up, down, pending, upConfigs, downConfigs, enabledIds: ids };
|
||||
return { up, down, pending, upConfigs, downConfigs, enabledIds: ids, allIds: ids };
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@ export const QuerySchema = schema.object({
|
|||
query: schema.maybe(schema.string()),
|
||||
filter: schema.maybe(schema.string()),
|
||||
tags: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
monitorType: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
monitorTypes: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
locations: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
projects: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
schedules: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
status: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
|
||||
fields: schema.maybe(schema.arrayOf(schema.string())),
|
||||
searchAfter: schema.maybe(schema.arrayOf(schema.string())),
|
||||
|
@ -51,19 +53,23 @@ export const getMonitors = (
|
|||
sortOrder,
|
||||
query,
|
||||
tags,
|
||||
monitorType,
|
||||
monitorTypes,
|
||||
locations,
|
||||
filter = '',
|
||||
fields,
|
||||
searchAfter,
|
||||
projects,
|
||||
schedules,
|
||||
} = request as MonitorsQuery;
|
||||
|
||||
const filterStr = getMonitorFilters({
|
||||
filter,
|
||||
monitorTypes: monitorType,
|
||||
monitorTypes,
|
||||
tags,
|
||||
locations,
|
||||
serviceLocations: syntheticsService.locations,
|
||||
projects,
|
||||
schedules,
|
||||
});
|
||||
|
||||
return savedObjectsClient.find({
|
||||
|
@ -85,13 +91,17 @@ export const getMonitorFilters = ({
|
|||
ports,
|
||||
filter,
|
||||
locations,
|
||||
projects,
|
||||
monitorTypes,
|
||||
schedules,
|
||||
serviceLocations,
|
||||
}: {
|
||||
filter?: string;
|
||||
tags?: string | string[];
|
||||
monitorTypes?: string | string[];
|
||||
locations?: string | string[];
|
||||
projects?: string | string[];
|
||||
schedules?: string | string[];
|
||||
ports?: string | string[];
|
||||
serviceLocations: ServiceLocations;
|
||||
}) => {
|
||||
|
@ -100,8 +110,10 @@ export const getMonitorFilters = ({
|
|||
return [
|
||||
filter,
|
||||
getKqlFilter({ field: 'tags', values: tags }),
|
||||
getKqlFilter({ field: 'project_id', values: projects }),
|
||||
getKqlFilter({ field: 'type', values: monitorTypes }),
|
||||
getKqlFilter({ field: 'locations.id', values: locationFilter }),
|
||||
getKqlFilter({ field: 'schedule.number', values: schedules }),
|
||||
]
|
||||
.filter((f) => !!f)
|
||||
.join(' AND ');
|
||||
|
@ -129,7 +141,7 @@ export const getKqlFilter = ({
|
|||
}
|
||||
|
||||
if (Array.isArray(values)) {
|
||||
return `${fieldKey}:"${values.join(`" ${operator} ${fieldKey}:"`)}"`;
|
||||
return ` (${fieldKey}:"${values.join(`" ${operator} ${fieldKey}:"`)}" )`;
|
||||
}
|
||||
|
||||
return `${fieldKey}:"${values}"`;
|
||||
|
@ -143,7 +155,7 @@ const parseLocationFilter = (serviceLocations: ServiceLocations, locations?: str
|
|||
if (Array.isArray(locations)) {
|
||||
return locations
|
||||
.map((loc) => findLocationItem(loc, serviceLocations)?.id ?? '')
|
||||
.filter((val) => !val);
|
||||
.filter((val) => !!val);
|
||||
}
|
||||
|
||||
return findLocationItem(locations, serviceLocations)?.id ?? '';
|
||||
|
@ -159,15 +171,18 @@ export const findLocationItem = (query: string, locations: ServiceLocations) =>
|
|||
* @param monitorQuery { MonitorsQuery }
|
||||
*/
|
||||
export const isMonitorsQueryFiltered = (monitorQuery: MonitorsQuery) => {
|
||||
const { query, tags, monitorType, locations, status, filter } = monitorQuery;
|
||||
const { query, tags, monitorTypes, locations, status, filter, projects, schedules } =
|
||||
monitorQuery;
|
||||
|
||||
return (
|
||||
!!query ||
|
||||
!!filter ||
|
||||
!!locations?.length ||
|
||||
!!monitorType?.length ||
|
||||
!!monitorTypes?.length ||
|
||||
!!tags?.length ||
|
||||
!!status?.length
|
||||
!!status?.length ||
|
||||
!!projects?.length ||
|
||||
!!schedules?.length
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -6,14 +6,26 @@
|
|||
*/
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { getAllMonitors } from '../../saved_objects/synthetics_monitor/get_all_monitors';
|
||||
import { isStatusEnabled } from '../../../common/runtime_types/monitor_management/alert_config';
|
||||
import { ConfigKey, MonitorOverviewItem, SyntheticsMonitor } from '../../../common/runtime_types';
|
||||
import {
|
||||
ConfigKey,
|
||||
EncryptedSyntheticsMonitor,
|
||||
MonitorOverviewItem,
|
||||
} from '../../../common/runtime_types';
|
||||
import { UMServerLibs } from '../../legacy_uptime/lib/lib';
|
||||
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types';
|
||||
import { API_URLS, SYNTHETICS_API_URLS } from '../../../common/constants';
|
||||
import { syntheticsMonitorType } from '../../legacy_uptime/lib/saved_objects/synthetics_monitor';
|
||||
import { getMonitorNotFoundResponse } from '../synthetics_service/service_errors';
|
||||
import { getMonitors, isMonitorsQueryFiltered, QuerySchema, SEARCH_FIELDS } from '../common';
|
||||
import {
|
||||
getMonitorFilters,
|
||||
getMonitors,
|
||||
isMonitorsQueryFiltered,
|
||||
MonitorsQuery,
|
||||
QuerySchema,
|
||||
SEARCH_FIELDS,
|
||||
} from '../common';
|
||||
|
||||
export const getSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
method: 'GET',
|
||||
|
@ -89,13 +101,24 @@ export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory =
|
|||
validate: {
|
||||
query: QuerySchema,
|
||||
},
|
||||
handler: async ({ request, savedObjectsClient }): Promise<any> => {
|
||||
const { sortField, sortOrder, query } = request.query;
|
||||
const finder = savedObjectsClient.createPointInTimeFinder<SyntheticsMonitor>({
|
||||
type: syntheticsMonitorType,
|
||||
sortField: sortField === 'status' ? `${ConfigKey.NAME}.keyword` : sortField,
|
||||
handler: async ({ request, savedObjectsClient, syntheticsMonitorClient }): Promise<any> => {
|
||||
const {
|
||||
sortField,
|
||||
sortOrder,
|
||||
perPage: 1000,
|
||||
query,
|
||||
locations: queriedLocations,
|
||||
} = request.query as MonitorsQuery;
|
||||
|
||||
const filtersStr = getMonitorFilters({
|
||||
...request.query,
|
||||
serviceLocations: syntheticsMonitorClient.syntheticsService.locations,
|
||||
});
|
||||
|
||||
const allMonitorConfigs = await getAllMonitors({
|
||||
sortOrder,
|
||||
filter: filtersStr,
|
||||
soClient: savedObjectsClient,
|
||||
sortField: sortField === 'status' ? `${ConfigKey.NAME}.keyword` : sortField,
|
||||
search: query ? `${query}*` : undefined,
|
||||
searchFields: SEARCH_FIELDS,
|
||||
});
|
||||
|
@ -104,29 +127,13 @@ export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory =
|
|||
let total = 0;
|
||||
const allMonitors: MonitorOverviewItem[] = [];
|
||||
|
||||
for await (const result of finder.find()) {
|
||||
/* collect all monitor ids for use
|
||||
* in filtering overview requests */
|
||||
result.saved_objects.forEach((monitor) => {
|
||||
const id = monitor.attributes[ConfigKey.MONITOR_QUERY_ID];
|
||||
const configId = monitor.attributes[ConfigKey.CONFIG_ID];
|
||||
allMonitorIds.push(configId);
|
||||
for (const { attributes } of allMonitorConfigs) {
|
||||
const configId = attributes[ConfigKey.CONFIG_ID];
|
||||
allMonitorIds.push(configId);
|
||||
|
||||
/* for each location, add a config item */
|
||||
const locations = monitor.attributes[ConfigKey.LOCATIONS];
|
||||
locations.forEach((location) => {
|
||||
const config = {
|
||||
id,
|
||||
configId,
|
||||
name: monitor.attributes[ConfigKey.NAME],
|
||||
location,
|
||||
isEnabled: monitor.attributes[ConfigKey.ENABLED],
|
||||
isStatusAlertEnabled: isStatusEnabled(monitor.attributes[ConfigKey.ALERT_CONFIG]),
|
||||
};
|
||||
allMonitors.push(config);
|
||||
total++;
|
||||
});
|
||||
});
|
||||
const monitorConfigsPerLocation = getOverviewConfigsPerLocation(attributes, queriedLocations);
|
||||
allMonitors.push(...monitorConfigsPerLocation);
|
||||
total += monitorConfigsPerLocation.length;
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -136,3 +143,34 @@ export const getSyntheticsMonitorOverviewRoute: SyntheticsRestApiRouteFactory =
|
|||
};
|
||||
},
|
||||
});
|
||||
|
||||
function getOverviewConfigsPerLocation(
|
||||
attributes: EncryptedSyntheticsMonitor,
|
||||
queriedLocations: string | string[] | undefined
|
||||
) {
|
||||
const id = attributes[ConfigKey.MONITOR_QUERY_ID];
|
||||
const configId = attributes[ConfigKey.CONFIG_ID];
|
||||
|
||||
/* for each location, add a config item */
|
||||
const locations = attributes[ConfigKey.LOCATIONS];
|
||||
const queriedLocationsArray =
|
||||
queriedLocations && !Array.isArray(queriedLocations) ? [queriedLocations] : queriedLocations;
|
||||
|
||||
/* exclude nob matching locations if location filter is present */
|
||||
const filteredLocations = queriedLocationsArray?.length
|
||||
? locations.filter(
|
||||
(loc) =>
|
||||
(loc.label && queriedLocationsArray.includes(loc.label)) ||
|
||||
queriedLocationsArray.includes(loc.id)
|
||||
)
|
||||
: locations;
|
||||
|
||||
return filteredLocations.map((location) => ({
|
||||
id,
|
||||
configId,
|
||||
name: attributes[ConfigKey.NAME],
|
||||
location,
|
||||
isEnabled: attributes[ConfigKey.ENABLED],
|
||||
isStatusAlertEnabled: isStatusEnabled(attributes[ConfigKey.ALERT_CONFIG]),
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -178,6 +178,7 @@ describe('current status route', () => {
|
|||
pending: 0,
|
||||
down: 1,
|
||||
enabledIds: ['id1', 'id2'],
|
||||
allIds: ['id1', 'id2'],
|
||||
up: 2,
|
||||
upConfigs: {
|
||||
'id1-Asia/Pacific - Japan': {
|
||||
|
@ -321,18 +322,24 @@ describe('current status route', () => {
|
|||
*
|
||||
* The expectation here is we will send the test client two separate "requests", one for each of the two IDs.
|
||||
*/
|
||||
const concernedLocations = [
|
||||
'Asia/Pacific - Japan',
|
||||
'Europe - Germany',
|
||||
'Asia/Pacific - Japan',
|
||||
];
|
||||
expect(
|
||||
await queryMonitorStatus(
|
||||
uptimeEsClient,
|
||||
times(10000).map((n) => 'Europe - Germany' + n),
|
||||
[...concernedLocations, ...times(9997).map((n) => 'Europe - Germany' + n)],
|
||||
{ from: 2500, to: 'now' },
|
||||
['id1', 'id2'],
|
||||
{ id1: ['Asia/Pacific - Japan'], id2: ['Europe - Germany', 'Asia/Pacific - Japan'] }
|
||||
{ id1: [concernedLocations[0]], id2: [concernedLocations[1], concernedLocations[2]] }
|
||||
)
|
||||
).toEqual({
|
||||
pending: 0,
|
||||
down: 1,
|
||||
enabledIds: ['id1', 'id2'],
|
||||
allIds: ['id1', 'id2'],
|
||||
up: 2,
|
||||
upConfigs: {
|
||||
'id1-Asia/Pacific - Japan': {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { intersection } from 'lodash';
|
||||
import datemath, { Unit } from '@kbn/datemath';
|
||||
import { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import {
|
||||
|
@ -19,7 +20,7 @@ import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
|
|||
import { UptimeEsClient } from '../../legacy_uptime/lib/lib';
|
||||
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
|
||||
import { ConfigKey } from '../../../common/runtime_types';
|
||||
import { QuerySchema, MonitorsQuery } from '../common';
|
||||
import { QuerySchema, MonitorsQuery, getMonitorFilters } from '../common';
|
||||
|
||||
/**
|
||||
* Helper function that converts a monitor's schedule to a value to use to generate
|
||||
|
@ -46,7 +47,7 @@ export async function getStatus(
|
|||
syntheticsMonitorClient: SyntheticsMonitorClient,
|
||||
params: MonitorsQuery
|
||||
) {
|
||||
const { query } = params;
|
||||
const { query, locations: queryLocations } = params;
|
||||
/**
|
||||
* Walk through all monitor saved objects, bucket IDs by disabled/enabled status.
|
||||
*
|
||||
|
@ -54,9 +55,14 @@ export async function getStatus(
|
|||
* latest ping for all enabled monitors.
|
||||
*/
|
||||
|
||||
const filtersStr = getMonitorFilters({
|
||||
...params,
|
||||
serviceLocations: syntheticsMonitorClient.syntheticsService.locations,
|
||||
});
|
||||
const allMonitors = await getAllMonitors({
|
||||
soClient,
|
||||
search: query ? `${query}*` : undefined,
|
||||
filter: filtersStr,
|
||||
fields: [
|
||||
ConfigKey.ENABLED,
|
||||
ConfigKey.LOCATIONS,
|
||||
|
@ -67,6 +73,7 @@ export async function getStatus(
|
|||
});
|
||||
|
||||
const {
|
||||
allIds,
|
||||
enabledIds,
|
||||
disabledCount,
|
||||
maxPeriod,
|
||||
|
@ -76,15 +83,23 @@ export async function getStatus(
|
|||
projectMonitorsCount,
|
||||
} = await processMonitors(allMonitors, server, soClient, syntheticsMonitorClient);
|
||||
|
||||
// Account for locations filter
|
||||
const queryLocationsArray =
|
||||
queryLocations && !Array.isArray(queryLocations) ? [queryLocations] : queryLocations;
|
||||
const listOfLocationAfterFilter = queryLocationsArray
|
||||
? intersection(listOfLocations, queryLocationsArray)
|
||||
: listOfLocations;
|
||||
|
||||
const { up, down, pending, upConfigs, downConfigs } = await queryMonitorStatus(
|
||||
uptimeEsClient,
|
||||
listOfLocations,
|
||||
listOfLocationAfterFilter,
|
||||
{ from: maxPeriod, to: 'now' },
|
||||
enabledIds,
|
||||
monitorLocationMap
|
||||
);
|
||||
|
||||
return {
|
||||
allIds,
|
||||
allMonitorsCount: allMonitors.length,
|
||||
disabledMonitorsCount,
|
||||
projectMonitorsCount,
|
||||
|
|
|
@ -27,12 +27,15 @@ export const getAllMonitors = async ({
|
|||
soClient,
|
||||
search,
|
||||
fields,
|
||||
filter,
|
||||
sortField,
|
||||
sortOrder,
|
||||
searchFields,
|
||||
}: {
|
||||
soClient: SavedObjectsClientContract;
|
||||
search?: string;
|
||||
} & Pick<SavedObjectsFindOptions, 'sortField' | 'sortOrder' | 'fields'>) => {
|
||||
filter?: string;
|
||||
} & Pick<SavedObjectsFindOptions, 'sortField' | 'sortOrder' | 'fields' | 'searchFields'>) => {
|
||||
const finder = soClient.createPointInTimeFinder({
|
||||
type: syntheticsMonitorType,
|
||||
perPage: 1000,
|
||||
|
@ -40,6 +43,8 @@ export const getAllMonitors = async ({
|
|||
sortField,
|
||||
sortOrder,
|
||||
fields,
|
||||
filter,
|
||||
searchFields,
|
||||
});
|
||||
|
||||
const hits: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitor>> = [];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue