mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* Finish implementing snapshot count redux code. * Replace GQL-powered Snapshot export with Redux/Rest-powered version. * Add tests for Snapshot API call. * Rename new test file from tsx to ts, it has no JSX. * Rename outdated snapshot file. * Update filter groups to use redux and add tags dropdown. * Delete obsolete graphql filter bar query. * Add fetch effect factory. * Use generic fetch effect factory to avoid code redundancy. * Infer isDisabled status from data for filter group buttons and disable when there are no items. * Fix removal of overview filter from previous rebase. * Rename generator-related functions from *saga to *effect. * WIP trying to make filters filterable. * WIP cleaning up. * Delete obsolete API test. * Add API test for filters endpoint. * Remove obsolete fields from overview filters. * Add functional testing attributes and delete a comment for filter popover. * Update obsolete unit test snapshots and test props for filter popover. * Fix broken types and delete obsolete test snapshots for filters api call. * Modify filters endpoint to adhere to np routing contracts. * Add functional test and associated helper functions for filters API. * Remove obsolete resolver function for filter bar. * Remove obsolete FilterBar type from graphql schema. * Delete static types generated for obsolete GQL schema types. * Delete obsolete fields from default filters state. * Delete obsolete method from graphql schema. * Add default values to unit test that requires complete app state mock. * Extract helper logic to dedicated module. * Finish working on adapter/helper tests. * Add state field for overview page search query. * Apply search kuery to filters. * Simplify creation of overview filter fetch actions and API call. * Add tests for overview filter action creators. * Simplify api query parameterizaton. * Improve a variable name. * Update formatting of file. * Improve a variable name. * Improve a variable name. * Simplify API endpoint typing. * Clean up helper code and rename some functions/vars. * Clean up parameterization of filter values. * Move function from dedicated file back to calling file. * Clean up naming in a function. * Move function from dedicated file to caller's file. * Modify interface of function return value. * Have function throw error when it receives invalid input instead of returning empty object. * Extract constant value to dedicated function value and remove parameter from function. * Clean up object declarations. * Rename a property. * Fix issue where function was not handling empty input. * Delete unnecessary snapshots. * Add message to internal server error response. * Fix broken type. * Delete type that was added as a result of a merge error. Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
e7a3354837
commit
05f6d7ba5d
61 changed files with 1394 additions and 260 deletions
|
@ -30,8 +30,6 @@ export interface Query {
|
|||
/** Fetch the most recent event data for a monitor ID, date range, location. */
|
||||
getLatestMonitors: Ping[];
|
||||
|
||||
getFilterBar?: FilterBar | null;
|
||||
|
||||
/** Fetches the current state of Uptime monitors for the given parameters. */
|
||||
getMonitorStates?: MonitorSummaryResult | null;
|
||||
/** Fetches details about the uptime index. */
|
||||
|
@ -467,21 +465,6 @@ export interface StatusData {
|
|||
/** The total down counts for this point. */
|
||||
total?: number | null;
|
||||
}
|
||||
/** The data used to enrich the filter bar. */
|
||||
export interface FilterBar {
|
||||
/** A series of monitor IDs in the heartbeat indices. */
|
||||
ids?: string[] | null;
|
||||
/** The location values users have configured for the agents. */
|
||||
locations?: string[] | null;
|
||||
/** The ports of the monitored endpoints. */
|
||||
ports?: number[] | null;
|
||||
/** The schemes used by the monitors. */
|
||||
schemes?: string[] | null;
|
||||
/** The possible status values contained in the indices. */
|
||||
statuses?: string[] | null;
|
||||
/** The list of URLs */
|
||||
urls?: string[] | null;
|
||||
}
|
||||
|
||||
/** The primary object returned for monitor states. */
|
||||
export interface MonitorSummaryResult {
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
*/
|
||||
|
||||
export * from './common';
|
||||
export * from './snapshot';
|
||||
export * from './monitor';
|
||||
export * from './overview_filters';
|
||||
export * from './snapshot';
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { OverviewFiltersType, OverviewFilters } from './overview_filters';
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
|
||||
export const OverviewFiltersType = t.type({
|
||||
locations: t.array(t.string),
|
||||
ports: t.array(t.number),
|
||||
schemes: t.array(t.string),
|
||||
tags: t.array(t.string),
|
||||
});
|
||||
|
||||
export type OverviewFilters = t.TypeOf<typeof OverviewFiltersType>;
|
|
@ -13,6 +13,7 @@ exports[`FilterPopover component does not show item list when loading 1`] = `
|
|||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
data-test-subj="filter-popover_test"
|
||||
display="inlineBlock"
|
||||
hasArrow={true}
|
||||
id="test"
|
||||
|
@ -49,6 +50,7 @@ exports[`FilterPopover component renders without errors for valid props 1`] = `
|
|||
/>
|
||||
}
|
||||
closePopover={[Function]}
|
||||
data-test-subj="filter-popover_test"
|
||||
display="inlineBlock"
|
||||
hasArrow={true}
|
||||
id="test"
|
||||
|
@ -83,6 +85,7 @@ exports[`FilterPopover component returns selected items on popover close 1`] = `
|
|||
</div>
|
||||
<div
|
||||
class="euiPopover euiPopover--anchorDownCenter euiPopover--withTitle"
|
||||
data-test-subj="filter-popover_test"
|
||||
id="test"
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`parseFiltersMap provides values from valid filter string 1`] = `
|
||||
Object {
|
||||
"locations": Array [
|
||||
"us-east-2",
|
||||
],
|
||||
"ports": Array [
|
||||
"5601",
|
||||
"80",
|
||||
],
|
||||
"schemes": Array [
|
||||
"http",
|
||||
"tcp",
|
||||
],
|
||||
"tags": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseFiltersMap returns an empty object for invalid filter 1`] = `"Unable to parse invalid filter string"`;
|
|
@ -19,7 +19,7 @@ describe('FilterPopover component', () => {
|
|||
props = {
|
||||
fieldName: 'foo',
|
||||
id: 'test',
|
||||
isLoading: false,
|
||||
loading: false,
|
||||
items: ['first', 'second', 'third', 'fourth'],
|
||||
onFilterFieldChange: jest.fn(),
|
||||
selectedItems: ['first', 'third'],
|
||||
|
@ -47,7 +47,7 @@ describe('FilterPopover component', () => {
|
|||
});
|
||||
|
||||
it('does not show item list when loading', () => {
|
||||
props.isLoading = true;
|
||||
props.loading = true;
|
||||
const wrapper = shallowWithIntl(<FilterPopover {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { parseFiltersMap } from '../parse_filter_map';
|
||||
|
||||
describe('parseFiltersMap', () => {
|
||||
it('provides values from valid filter string', () => {
|
||||
expect(
|
||||
parseFiltersMap(
|
||||
'[["url.port",["5601","80"]],["observer.geo.name",["us-east-2"]],["monitor.type",["http","tcp"]]]'
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('returns an empty object for invalid filter', () => {
|
||||
expect(() => parseFiltersMap('some invalid string')).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
|
@ -5,35 +5,49 @@
|
|||
*/
|
||||
|
||||
import { EuiFilterGroup } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { get } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FilterBar as FilterBarType } from '../../../../common/graphql/types';
|
||||
import { UptimeGraphQLQueryProps, withUptimeGraphQL } from '../../higher_order';
|
||||
import { filterBarQuery } from '../../../queries';
|
||||
import { connect } from 'react-redux';
|
||||
import { FilterPopoverProps, FilterPopover } from './filter_popover';
|
||||
import { FilterStatusButton } from './filter_status_button';
|
||||
import { OverviewFilters } from '../../../../common/runtime_types';
|
||||
import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions';
|
||||
import { AppState } from '../../../state';
|
||||
import { useUrlParams } from '../../../hooks';
|
||||
import { parseFiltersMap } from './parse_filter_map';
|
||||
|
||||
interface FilterBarQueryResult {
|
||||
filters?: FilterBarType;
|
||||
interface OwnProps {
|
||||
currentFilter: any;
|
||||
onFilterUpdate: any;
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
filters?: string;
|
||||
statusFilter?: string;
|
||||
}
|
||||
|
||||
interface FilterBarDropdownsProps {
|
||||
currentFilter: string;
|
||||
onFilterUpdate: (kuery: string) => void;
|
||||
interface StoreProps {
|
||||
esKuery: string;
|
||||
lastRefresh: number;
|
||||
loading: boolean;
|
||||
overviewFilters: OverviewFilters;
|
||||
}
|
||||
|
||||
type Props = UptimeGraphQLQueryProps<FilterBarQueryResult> & FilterBarDropdownsProps;
|
||||
interface DispatchProps {
|
||||
loadFilterGroup: typeof fetchOverviewFilters;
|
||||
}
|
||||
|
||||
export const FilterGroupComponent = ({
|
||||
loading: isLoading,
|
||||
type Props = OwnProps & StoreProps & DispatchProps;
|
||||
|
||||
type PresentationalComponentProps = Pick<StoreProps, 'overviewFilters' | 'loading'> &
|
||||
Pick<OwnProps, 'currentFilter' | 'onFilterUpdate'>;
|
||||
|
||||
export const PresentationalComponent: React.FC<PresentationalComponentProps> = ({
|
||||
currentFilter,
|
||||
data,
|
||||
overviewFilters,
|
||||
loading,
|
||||
onFilterUpdate,
|
||||
}: Props) => {
|
||||
const locations = get<string[]>(data, 'filterBar.locations', []);
|
||||
const ports = get<string[]>(data, 'filterBar.ports', []);
|
||||
const schemes = get<string[]>(data, 'filterBar.schemes', []);
|
||||
}) => {
|
||||
const { locations, ports, schemes, tags } = overviewFilters;
|
||||
|
||||
let filterKueries: Map<string, string[]>;
|
||||
try {
|
||||
|
@ -67,36 +81,50 @@ export const FilterGroupComponent = ({
|
|||
|
||||
const filterPopoverProps: FilterPopoverProps[] = [
|
||||
{
|
||||
loading,
|
||||
onFilterFieldChange,
|
||||
fieldName: 'observer.geo.name',
|
||||
id: 'location',
|
||||
isLoading,
|
||||
items: locations,
|
||||
onFilterFieldChange,
|
||||
selectedItems: getSelectedItems('observer.geo.name'),
|
||||
title: i18n.translate('xpack.uptime.filterBar.options.location.name', {
|
||||
defaultMessage: 'Location',
|
||||
}),
|
||||
},
|
||||
{
|
||||
loading,
|
||||
onFilterFieldChange,
|
||||
fieldName: 'url.port',
|
||||
id: 'port',
|
||||
isLoading,
|
||||
items: ports,
|
||||
onFilterFieldChange,
|
||||
disabled: ports.length === 0,
|
||||
items: ports.map((p: number) => p.toString()),
|
||||
selectedItems: getSelectedItems('url.port'),
|
||||
title: i18n.translate('xpack.uptime.filterBar.options.portLabel', { defaultMessage: 'Port' }),
|
||||
},
|
||||
{
|
||||
loading,
|
||||
onFilterFieldChange,
|
||||
fieldName: 'monitor.type',
|
||||
id: 'scheme',
|
||||
isLoading,
|
||||
disabled: schemes.length === 0,
|
||||
items: schemes,
|
||||
onFilterFieldChange,
|
||||
selectedItems: getSelectedItems('monitor.type'),
|
||||
title: i18n.translate('xpack.uptime.filterBar.options.schemeLabel', {
|
||||
defaultMessage: 'Scheme',
|
||||
}),
|
||||
},
|
||||
{
|
||||
loading,
|
||||
onFilterFieldChange,
|
||||
fieldName: 'tags',
|
||||
id: 'tags',
|
||||
disabled: tags.length === 0,
|
||||
items: tags,
|
||||
selectedItems: getSelectedItems('tags'),
|
||||
title: i18n.translate('xpack.uptime.filterBar.options.tagsLabel', {
|
||||
defaultMessage: 'Tags',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
@ -124,7 +152,59 @@ export const FilterGroupComponent = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const FilterGroup = withUptimeGraphQL<FilterBarQueryResult, FilterBarDropdownsProps>(
|
||||
FilterGroupComponent,
|
||||
filterBarQuery
|
||||
);
|
||||
export const Container: React.FC<Props> = ({
|
||||
currentFilter,
|
||||
esKuery,
|
||||
filters,
|
||||
loading,
|
||||
loadFilterGroup,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
overviewFilters,
|
||||
statusFilter,
|
||||
onFilterUpdate,
|
||||
}: Props) => {
|
||||
const [getUrlParams] = useUrlParams();
|
||||
const { filters: urlFilters } = getUrlParams();
|
||||
useEffect(() => {
|
||||
const filterSelections = parseFiltersMap(urlFilters);
|
||||
loadFilterGroup({
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
locations: filterSelections.locations ?? [],
|
||||
ports: filterSelections.ports ?? [],
|
||||
schemes: filterSelections.schemes ?? [],
|
||||
search: esKuery,
|
||||
statusFilter,
|
||||
tags: filterSelections.tags ?? [],
|
||||
});
|
||||
}, [dateRangeStart, dateRangeEnd, esKuery, filters, statusFilter, urlFilters, loadFilterGroup]);
|
||||
return (
|
||||
<PresentationalComponent
|
||||
currentFilter={currentFilter}
|
||||
overviewFilters={overviewFilters}
|
||||
loading={loading}
|
||||
onFilterUpdate={onFilterUpdate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
overviewFilters: { loading, filters },
|
||||
ui: { esKuery, lastRefresh },
|
||||
}: AppState): StoreProps => ({
|
||||
esKuery,
|
||||
overviewFilters: filters,
|
||||
lastRefresh,
|
||||
loading,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: any): DispatchProps => ({
|
||||
loadFilterGroup: (payload: GetOverviewFiltersPayload) => dispatch(fetchOverviewFilters(payload)),
|
||||
});
|
||||
|
||||
export const FilterGroup = connect<StoreProps, DispatchProps, OwnProps>(
|
||||
// @ts-ignore connect is expecting null | undefined for some reason
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Container);
|
||||
|
|
|
@ -14,7 +14,8 @@ import { LocationLink } from '../monitor_list';
|
|||
export interface FilterPopoverProps {
|
||||
fieldName: string;
|
||||
id: string;
|
||||
isLoading: boolean;
|
||||
loading: boolean;
|
||||
disabled?: boolean;
|
||||
items: string[];
|
||||
onFilterFieldChange: (fieldName: string, values: string[]) => void;
|
||||
selectedItems: string[];
|
||||
|
@ -27,7 +28,8 @@ const isItemSelected = (selectedItems: string[], item: string): 'on' | undefined
|
|||
export const FilterPopover = ({
|
||||
fieldName,
|
||||
id,
|
||||
isLoading,
|
||||
disabled,
|
||||
loading,
|
||||
items,
|
||||
onFilterFieldChange,
|
||||
selectedItems,
|
||||
|
@ -48,10 +50,10 @@ export const FilterPopover = ({
|
|||
}, [searchQuery, items]);
|
||||
|
||||
return (
|
||||
// @ts-ignore zIndex prop is not described in the typing yet
|
||||
<EuiPopover
|
||||
button={
|
||||
<UptimeFilterButton
|
||||
isDisabled={disabled}
|
||||
isSelected={tempSelectedItems.length > 0}
|
||||
numFilters={items.length}
|
||||
numActiveFilters={tempSelectedItems.length}
|
||||
|
@ -66,6 +68,7 @@ export const FilterPopover = ({
|
|||
setIsOpen(false);
|
||||
onFilterFieldChange(fieldName, tempSelectedItems);
|
||||
}}
|
||||
data-test-subj={`filter-popover_${id}`}
|
||||
id={id}
|
||||
isOpen={isOpen}
|
||||
ownFocus={true}
|
||||
|
@ -77,7 +80,7 @@ export const FilterPopover = ({
|
|||
disabled={items.length === 0}
|
||||
onSearch={query => setSearchQuery(query)}
|
||||
placeholder={
|
||||
isLoading
|
||||
loading
|
||||
? i18n.translate('xpack.uptime.filterPopout.loadingMessage', {
|
||||
defaultMessage: 'Loading...',
|
||||
})
|
||||
|
@ -90,10 +93,11 @@ export const FilterPopover = ({
|
|||
}
|
||||
/>
|
||||
</EuiPopoverTitle>
|
||||
{!isLoading &&
|
||||
{!loading &&
|
||||
itemsToDisplay.map(item => (
|
||||
<EuiFilterSelectItem
|
||||
checked={isItemSelected(tempSelectedItems, item)}
|
||||
data-test-subj={`filter-popover-item_${item}`}
|
||||
key={item}
|
||||
onClick={() => toggleSelectedItems(item, tempSelectedItems, setTempSelectedItems)}
|
||||
>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useUrlParams } from '../../../hooks';
|
|||
export interface FilterStatusButtonProps {
|
||||
content: string;
|
||||
dataTestSubj: string;
|
||||
isDisabled?: boolean;
|
||||
value: string;
|
||||
withNext: boolean;
|
||||
}
|
||||
|
@ -18,6 +19,7 @@ export interface FilterStatusButtonProps {
|
|||
export const FilterStatusButton = ({
|
||||
content,
|
||||
dataTestSubj,
|
||||
isDisabled,
|
||||
value,
|
||||
withNext,
|
||||
}: FilterStatusButtonProps) => {
|
||||
|
@ -27,6 +29,7 @@ export const FilterStatusButton = ({
|
|||
<EuiFilterButton
|
||||
data-test-subj={dataTestSubj}
|
||||
hasActiveFilters={urlValue === value}
|
||||
isDisabled={isDisabled}
|
||||
onClick={() => {
|
||||
const nextFilter = { statusFilter: urlValue === value ? '' : value, pagination: '' };
|
||||
setUrlParams(nextFilter);
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
interface FilterField {
|
||||
name: string;
|
||||
fieldName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the only filter fields we are looking to catch at the moment.
|
||||
* If your code needs to support custom fields, introduce a second parameter to
|
||||
* `parseFiltersMap` to take a list of FilterField objects.
|
||||
*/
|
||||
const filterWhitelist: FilterField[] = [
|
||||
{ name: 'ports', fieldName: 'url.port' },
|
||||
{ name: 'locations', fieldName: 'observer.geo.name' },
|
||||
{ name: 'tags', fieldName: 'tags' },
|
||||
{ name: 'schemes', fieldName: 'monitor.type' },
|
||||
];
|
||||
|
||||
export const parseFiltersMap = (filterMapString: string) => {
|
||||
if (!filterMapString) {
|
||||
return {};
|
||||
}
|
||||
const filterSlices: { [key: string]: any } = {};
|
||||
try {
|
||||
const map = new Map<string, string[]>(JSON.parse(filterMapString));
|
||||
filterWhitelist.forEach(({ name, fieldName }) => {
|
||||
filterSlices[name] = map.get(fieldName) ?? [];
|
||||
});
|
||||
return filterSlices;
|
||||
} catch {
|
||||
throw new Error('Unable to parse invalid filter string');
|
||||
}
|
||||
};
|
|
@ -8,6 +8,7 @@ import { EuiFilterButton } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
|
||||
interface UptimeFilterButtonProps {
|
||||
isDisabled?: boolean;
|
||||
isSelected: boolean;
|
||||
numFilters: number;
|
||||
numActiveFilters: number;
|
||||
|
@ -16,6 +17,7 @@ interface UptimeFilterButtonProps {
|
|||
}
|
||||
|
||||
export const UptimeFilterButton = ({
|
||||
isDisabled,
|
||||
isSelected,
|
||||
numFilters,
|
||||
numActiveFilters,
|
||||
|
@ -25,6 +27,7 @@ export const UptimeFilterButton = ({
|
|||
<EuiFilterButton
|
||||
hasActiveFilters={numActiveFilters !== 0}
|
||||
iconType="arrowDown"
|
||||
isDisabled={isDisabled}
|
||||
isSelected={isSelected}
|
||||
numActiveFilters={numActiveFilters}
|
||||
numFilters={numFilters}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`parameterizeValues parameterizes provided values for multiple fields 1`] = `"foo=bar&foo=baz&bar=foo&bar=baz"`;
|
||||
|
||||
exports[`parameterizeValues parameterizes the provided values for a given field name 1`] = `"foo=bar&foo=baz"`;
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { parameterizeValues } from '../parameterize_values';
|
||||
|
||||
describe('parameterizeValues', () => {
|
||||
let params: URLSearchParams;
|
||||
|
||||
beforeEach(() => {
|
||||
params = new URLSearchParams();
|
||||
});
|
||||
|
||||
it('parameterizes the provided values for a given field name', () => {
|
||||
parameterizeValues(params, { foo: ['bar', 'baz'] });
|
||||
expect(params.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('parameterizes provided values for multiple fields', () => {
|
||||
parameterizeValues(params, { foo: ['bar', 'baz'], bar: ['foo', 'baz'] });
|
||||
expect(params.toString()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('returns an empty string when there are no values provided', () => {
|
||||
parameterizeValues(params, { foo: [] });
|
||||
expect(params.toString()).toBe('');
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@ export { convertMicrosecondsToMilliseconds } from './convert_measurements';
|
|||
export * from './observability_integration';
|
||||
export { getApiPath } from './get_api_path';
|
||||
export { getChartDateLabel } from './charts';
|
||||
export { parameterizeValues } from './parameterize_values';
|
||||
export { seriesHasDownValues } from './series_has_down_values';
|
||||
export { stringifyKueries } from './stringify_kueries';
|
||||
export { toStaticIndexPattern } from './to_static_index_pattern';
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const parameterizeValues = (
|
||||
params: URLSearchParams,
|
||||
obj: Record<string, string[]>
|
||||
): void => {
|
||||
Object.keys(obj).forEach(key => {
|
||||
obj[key].forEach(val => {
|
||||
params.append(key, val);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -22,6 +22,8 @@ import { stringifyUrlParams } from '../lib/helper/stringify_url_params';
|
|||
import { useTrackPageview } from '../../../infra/public';
|
||||
import { combineFiltersAndUserSearch, stringifyKueries, toStaticIndexPattern } from '../lib/helper';
|
||||
import { AutocompleteProviderRegister, esKuery } from '../../../../../../src/plugins/data/public';
|
||||
import { store } from '../state';
|
||||
import { setEsKueryString } from '../state/actions';
|
||||
import { PageHeader } from './page_header';
|
||||
|
||||
interface OverviewPageProps {
|
||||
|
@ -64,7 +66,6 @@ export const OverviewPage = ({ autocomplete, setBreadcrumbs }: Props) => {
|
|||
useTrackPageview({ app: 'uptime', path: 'overview' });
|
||||
useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 });
|
||||
|
||||
const filterQueryString = search || '';
|
||||
let error: any;
|
||||
let kueryString: string = '';
|
||||
try {
|
||||
|
@ -76,6 +77,7 @@ export const OverviewPage = ({ autocomplete, setBreadcrumbs }: Props) => {
|
|||
kueryString = '';
|
||||
}
|
||||
|
||||
const filterQueryString = search || '';
|
||||
let filters: any | undefined;
|
||||
try {
|
||||
if (filterQueryString || urlFilters) {
|
||||
|
@ -85,6 +87,15 @@ export const OverviewPage = ({ autocomplete, setBreadcrumbs }: Props) => {
|
|||
const ast = esKuery.fromKueryExpression(combinedFilterString);
|
||||
const elasticsearchQuery = esKuery.toElasticsearchQuery(ast, staticIndexPattern);
|
||||
filters = JSON.stringify(elasticsearchQuery);
|
||||
const searchDSL: string = filterQueryString
|
||||
? JSON.stringify(
|
||||
esKuery.toElasticsearchQuery(
|
||||
esKuery.fromKueryExpression(filterQueryString),
|
||||
staticIndexPattern
|
||||
)
|
||||
)
|
||||
: '';
|
||||
store.dispatch(setEsKueryString(searchDSL));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -110,13 +121,13 @@ export const OverviewPage = ({ autocomplete, setBreadcrumbs }: Props) => {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItemStyled grow={true}>
|
||||
<FilterGroup
|
||||
{...sharedProps}
|
||||
currentFilter={urlFilters}
|
||||
onFilterUpdate={(filtersKuery: string) => {
|
||||
if (urlFilters !== filtersKuery) {
|
||||
updateUrl({ filters: filtersKuery, pagination: '' });
|
||||
}
|
||||
}}
|
||||
variables={sharedProps}
|
||||
/>
|
||||
</EuiFlexItemStyled>
|
||||
{error && <OverviewPageParsingErrorCallout error={error} />}
|
||||
|
|
|
@ -1,23 +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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const filterBarQueryString = `
|
||||
query FilterBar($dateRangeStart: String!, $dateRangeEnd: String!) {
|
||||
filterBar: getFilterBar(dateRangeStart: $dateRangeStart, dateRangeEnd: $dateRangeEnd) {
|
||||
ids
|
||||
locations
|
||||
ports
|
||||
schemes
|
||||
urls
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const filterBarQuery = gql`
|
||||
${filterBarQueryString}
|
||||
`;
|
|
@ -5,6 +5,5 @@
|
|||
*/
|
||||
|
||||
export { docCountQuery, docCountQueryString } from './doc_count_query';
|
||||
export { filterBarQuery, filterBarQueryString } from './filter_bar_query';
|
||||
export { monitorChartsQuery, monitorChartsQueryString } from './monitor_charts_query';
|
||||
export { pingsQuery, pingsQueryString } from './pings_query';
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`overview filters action creators creates a fail action 1`] = `
|
||||
Object {
|
||||
"payload": [Error: There was an error retrieving the overview filters],
|
||||
"type": "FETCH_OVERVIEW_FILTERS_FAIL",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`overview filters action creators creates a get action 1`] = `
|
||||
Object {
|
||||
"payload": Object {
|
||||
"dateRangeEnd": "now",
|
||||
"dateRangeStart": "now-15m",
|
||||
"locations": Array [
|
||||
"fairbanks",
|
||||
"tokyo",
|
||||
],
|
||||
"ports": Array [
|
||||
"80",
|
||||
],
|
||||
"schemes": Array [
|
||||
"http",
|
||||
"tcp",
|
||||
],
|
||||
"search": "",
|
||||
"statusFilter": "down",
|
||||
"tags": Array [
|
||||
"api",
|
||||
"dev",
|
||||
],
|
||||
},
|
||||
"type": "FETCH_OVERVIEW_FILTERS",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`overview filters action creators creates a success action 1`] = `
|
||||
Object {
|
||||
"payload": Object {
|
||||
"locations": Array [
|
||||
"fairbanks",
|
||||
"tokyo",
|
||||
"london",
|
||||
],
|
||||
"ports": Array [
|
||||
80,
|
||||
443,
|
||||
],
|
||||
"schemes": Array [
|
||||
"http",
|
||||
"tcp",
|
||||
],
|
||||
"tags": Array [
|
||||
"api",
|
||||
"dev",
|
||||
"prod",
|
||||
],
|
||||
},
|
||||
"type": "FETCH_OVERVIEW_FILTERS_SUCCESS",
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
fetchOverviewFilters,
|
||||
fetchOverviewFiltersSuccess,
|
||||
fetchOverviewFiltersFail,
|
||||
} from '../overview_filters';
|
||||
|
||||
describe('overview filters action creators', () => {
|
||||
it('creates a get action', () => {
|
||||
expect(
|
||||
fetchOverviewFilters({
|
||||
dateRangeStart: 'now-15m',
|
||||
dateRangeEnd: 'now',
|
||||
statusFilter: 'down',
|
||||
search: '',
|
||||
locations: ['fairbanks', 'tokyo'],
|
||||
ports: ['80'],
|
||||
schemes: ['http', 'tcp'],
|
||||
tags: ['api', 'dev'],
|
||||
})
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('creates a success action', () => {
|
||||
expect(
|
||||
fetchOverviewFiltersSuccess({
|
||||
locations: ['fairbanks', 'tokyo', 'london'],
|
||||
ports: [80, 443],
|
||||
schemes: ['http', 'tcp'],
|
||||
tags: ['api', 'dev', 'prod'],
|
||||
})
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('creates a fail action', () => {
|
||||
expect(
|
||||
fetchOverviewFiltersFail(new Error('There was an error retrieving the overview filters'))
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export * from './overview_filters';
|
||||
export * from './snapshot';
|
||||
export * from './ui';
|
||||
export * from './monitor_status';
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { OverviewFilters } from '../../../common/runtime_types';
|
||||
|
||||
export const FETCH_OVERVIEW_FILTERS = 'FETCH_OVERVIEW_FILTERS';
|
||||
export const FETCH_OVERVIEW_FILTERS_FAIL = 'FETCH_OVERVIEW_FILTERS_FAIL';
|
||||
export const FETCH_OVERVIEW_FILTERS_SUCCESS = 'FETCH_OVERVIEW_FILTERS_SUCCESS';
|
||||
|
||||
export interface GetOverviewFiltersPayload {
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
locations: string[];
|
||||
ports: string[];
|
||||
schemes: string[];
|
||||
search?: string;
|
||||
statusFilter?: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface GetOverviewFiltersFetchAction {
|
||||
type: typeof FETCH_OVERVIEW_FILTERS;
|
||||
payload: GetOverviewFiltersPayload;
|
||||
}
|
||||
|
||||
interface GetOverviewFiltersSuccessAction {
|
||||
type: typeof FETCH_OVERVIEW_FILTERS_SUCCESS;
|
||||
payload: OverviewFilters;
|
||||
}
|
||||
|
||||
interface GetOverviewFiltersFailAction {
|
||||
type: typeof FETCH_OVERVIEW_FILTERS_FAIL;
|
||||
payload: Error;
|
||||
}
|
||||
|
||||
export type OverviewFiltersAction =
|
||||
| GetOverviewFiltersFetchAction
|
||||
| GetOverviewFiltersSuccessAction
|
||||
| GetOverviewFiltersFailAction;
|
||||
|
||||
export const fetchOverviewFilters = (
|
||||
payload: GetOverviewFiltersPayload
|
||||
): GetOverviewFiltersFetchAction => ({
|
||||
type: FETCH_OVERVIEW_FILTERS,
|
||||
payload,
|
||||
});
|
||||
|
||||
export const fetchOverviewFiltersFail = (error: Error): GetOverviewFiltersFailAction => ({
|
||||
type: FETCH_OVERVIEW_FILTERS_FAIL,
|
||||
payload: error,
|
||||
});
|
||||
|
||||
export const fetchOverviewFiltersSuccess = (
|
||||
filters: OverviewFilters
|
||||
): GetOverviewFiltersSuccessAction => ({
|
||||
type: FETCH_OVERVIEW_FILTERS_SUCCESS,
|
||||
payload: filters,
|
||||
});
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Snapshot } from '../../../common/runtime_types';
|
||||
|
||||
export const FETCH_SNAPSHOT_COUNT = 'FETCH_SNAPSHOT_COUNT';
|
||||
export const FETCH_SNAPSHOT_COUNT_FAIL = 'FETCH_SNAPSHOT_COUNT_FAIL';
|
||||
export const FETCH_SNAPSHOT_COUNT_SUCCESS = 'FETCH_SNAPSHOT_COUNT_SUCCESS';
|
||||
|
|
|
@ -16,6 +16,8 @@ export const setBasePath = createAction<string>('SET BASE PATH');
|
|||
|
||||
export const triggerAppRefresh = createAction<number>('REFRESH APP');
|
||||
|
||||
export const setEsKueryString = createAction<string>('SET ES KUERY STRING');
|
||||
|
||||
export const toggleIntegrationsPopover = createAction<PopoverState>(
|
||||
'TOGGLE INTEGRATION POPOVER STATE'
|
||||
);
|
||||
|
|
|
@ -5,5 +5,6 @@
|
|||
*/
|
||||
|
||||
export * from './monitor';
|
||||
export * from './overview_filters';
|
||||
export * from './snapshot';
|
||||
export * from './monitor_status';
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ThrowReporter } from 'io-ts/lib/ThrowReporter';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import { GetOverviewFiltersPayload } from '../actions/overview_filters';
|
||||
import { getApiPath, parameterizeValues } from '../../lib/helper';
|
||||
import { OverviewFiltersType } from '../../../common/runtime_types';
|
||||
|
||||
type ApiRequest = GetOverviewFiltersPayload & {
|
||||
basePath: string;
|
||||
};
|
||||
|
||||
export const fetchOverviewFilters = async ({
|
||||
basePath,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
search,
|
||||
schemes,
|
||||
locations,
|
||||
ports,
|
||||
tags,
|
||||
}: ApiRequest) => {
|
||||
const url = getApiPath(`/api/uptime/filters`, basePath);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
});
|
||||
|
||||
if (search) {
|
||||
params.append('search', search);
|
||||
}
|
||||
|
||||
parameterizeValues(params, { schemes, locations, ports, tags });
|
||||
|
||||
const response = await fetch(`${url}?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const responseData = await response.json();
|
||||
const decoded = OverviewFiltersType.decode(responseData);
|
||||
|
||||
ThrowReporter.report(decoded);
|
||||
if (isRight(decoded)) {
|
||||
return decoded.right;
|
||||
}
|
||||
throw new Error('`getOverviewFilters` response did not correspond to expected type');
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { call, put, select } from 'redux-saga/effects';
|
||||
import { Action } from 'redux-actions';
|
||||
import { getBasePath } from '../selectors';
|
||||
|
||||
/**
|
||||
* Factory function for a fetch effect. It expects three action creators,
|
||||
* one to call for a fetch, one to call for success, and one to handle failures.
|
||||
* @param fetch creates a fetch action
|
||||
* @param success creates a success action
|
||||
* @param fail creates a failure action
|
||||
* @template T the action type expected by the fetch action
|
||||
* @template R the type that the API request should return on success
|
||||
* @template S tye type of the success action
|
||||
* @template F the type of the failure action
|
||||
*/
|
||||
export function fetchEffectFactory<T, R, S, F>(
|
||||
fetch: (request: T) => Promise<R>,
|
||||
success: (response: R) => Action<S>,
|
||||
fail: (error: Error) => Action<F>
|
||||
) {
|
||||
return function*(action: Action<T>) {
|
||||
try {
|
||||
if (!action.payload) {
|
||||
yield put(fail(new Error('Cannot fetch snapshot for undefined parameters.')));
|
||||
return;
|
||||
}
|
||||
const {
|
||||
payload: { ...params },
|
||||
} = action;
|
||||
const basePath = yield select(getBasePath);
|
||||
const response = yield call(fetch, { ...params, basePath });
|
||||
yield put(success(response));
|
||||
} catch (error) {
|
||||
yield put(fail(error));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
import { fork } from 'redux-saga/effects';
|
||||
import { fetchMonitorDetailsEffect } from './monitor';
|
||||
import { fetchSnapshotCountSaga } from './snapshot';
|
||||
import { fetchOverviewFiltersEffect } from './overview_filters';
|
||||
import { fetchSnapshotCountEffect } from './snapshot';
|
||||
import { fetchMonitorStatusEffect } from './monitor_status';
|
||||
|
||||
export function* rootEffect() {
|
||||
yield fork(fetchMonitorDetailsEffect);
|
||||
yield fork(fetchSnapshotCountSaga);
|
||||
yield fork(fetchSnapshotCountEffect);
|
||||
yield fork(fetchOverviewFiltersEffect);
|
||||
yield fork(fetchMonitorStatusEffect);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import {
|
||||
FETCH_OVERVIEW_FILTERS,
|
||||
fetchOverviewFiltersFail,
|
||||
fetchOverviewFiltersSuccess,
|
||||
} from '../actions';
|
||||
import { fetchOverviewFilters } from '../api';
|
||||
import { fetchEffectFactory } from './fetch_effect';
|
||||
|
||||
export function* fetchOverviewFiltersEffect() {
|
||||
yield takeLatest(
|
||||
FETCH_OVERVIEW_FILTERS,
|
||||
fetchEffectFactory(fetchOverviewFilters, fetchOverviewFiltersSuccess, fetchOverviewFiltersFail)
|
||||
);
|
||||
}
|
|
@ -4,42 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { call, put, takeLatest, select } from 'redux-saga/effects';
|
||||
import { Action } from 'redux-actions';
|
||||
import { takeLatest } from 'redux-saga/effects';
|
||||
import {
|
||||
FETCH_SNAPSHOT_COUNT,
|
||||
GetSnapshotPayload,
|
||||
fetchSnapshotCountFail,
|
||||
fetchSnapshotCountSuccess,
|
||||
} from '../actions';
|
||||
import { fetchSnapshotCount } from '../api';
|
||||
import { getBasePath } from '../selectors';
|
||||
import { fetchEffectFactory } from './fetch_effect';
|
||||
|
||||
function* snapshotSaga(action: Action<GetSnapshotPayload>) {
|
||||
try {
|
||||
if (!action.payload) {
|
||||
yield put(
|
||||
fetchSnapshotCountFail(new Error('Cannot fetch snapshot for undefined parameters.'))
|
||||
);
|
||||
return;
|
||||
}
|
||||
const {
|
||||
payload: { dateRangeStart, dateRangeEnd, filters, statusFilter },
|
||||
} = action;
|
||||
const basePath = yield select(getBasePath);
|
||||
const response = yield call(fetchSnapshotCount, {
|
||||
basePath,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
filters,
|
||||
statusFilter,
|
||||
});
|
||||
yield put(fetchSnapshotCountSuccess(response));
|
||||
} catch (error) {
|
||||
yield put(fetchSnapshotCountFail(error));
|
||||
}
|
||||
}
|
||||
|
||||
export function* fetchSnapshotCountSaga() {
|
||||
yield takeLatest(FETCH_SNAPSHOT_COUNT, snapshotSaga);
|
||||
export function* fetchSnapshotCountEffect() {
|
||||
yield takeLatest(
|
||||
FETCH_SNAPSHOT_COUNT,
|
||||
fetchEffectFactory(fetchSnapshotCount, fetchSnapshotCountSuccess, fetchSnapshotCountFail)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
exports[`ui reducer adds integration popover status to state 1`] = `
|
||||
Object {
|
||||
"basePath": "",
|
||||
"esKuery": "",
|
||||
"integrationsPopoverOpen": Object {
|
||||
"id": "popover-2",
|
||||
"open": true,
|
||||
|
@ -14,6 +15,7 @@ Object {
|
|||
exports[`ui reducer sets the application's base path 1`] = `
|
||||
Object {
|
||||
"basePath": "yyz",
|
||||
"esKuery": "",
|
||||
"integrationsPopoverOpen": null,
|
||||
"lastRefresh": 125,
|
||||
}
|
||||
|
@ -22,6 +24,7 @@ Object {
|
|||
exports[`ui reducer updates the refresh value 1`] = `
|
||||
Object {
|
||||
"basePath": "abc",
|
||||
"esKuery": "",
|
||||
"integrationsPopoverOpen": null,
|
||||
"lastRefresh": 125,
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ describe('ui reducer', () => {
|
|||
uiReducer(
|
||||
{
|
||||
basePath: 'abc',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
lastRefresh: 125,
|
||||
},
|
||||
|
@ -32,6 +33,7 @@ describe('ui reducer', () => {
|
|||
uiReducer(
|
||||
{
|
||||
basePath: '',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
lastRefresh: 125,
|
||||
},
|
||||
|
@ -46,6 +48,7 @@ describe('ui reducer', () => {
|
|||
uiReducer(
|
||||
{
|
||||
basePath: 'abc',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
lastRefresh: 125,
|
||||
},
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
|
||||
import { combineReducers } from 'redux';
|
||||
import { monitorReducer } from './monitor';
|
||||
import { overviewFiltersReducer } from './overview_filters';
|
||||
import { snapshotReducer } from './snapshot';
|
||||
import { uiReducer } from './ui';
|
||||
import { monitorStatusReducer } from './monitor_status';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
monitor: monitorReducer,
|
||||
overviewFilters: overviewFiltersReducer,
|
||||
snapshot: snapshotReducer,
|
||||
ui: uiReducer,
|
||||
monitorStatus: monitorStatusReducer,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { OverviewFilters } from '../../../common/runtime_types';
|
||||
import {
|
||||
FETCH_OVERVIEW_FILTERS,
|
||||
FETCH_OVERVIEW_FILTERS_FAIL,
|
||||
FETCH_OVERVIEW_FILTERS_SUCCESS,
|
||||
OverviewFiltersAction,
|
||||
} from '../actions';
|
||||
|
||||
export interface OverviewFiltersState {
|
||||
filters: OverviewFilters;
|
||||
errors: Error[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const initialState: OverviewFiltersState = {
|
||||
filters: {
|
||||
locations: [],
|
||||
ports: [],
|
||||
schemes: [],
|
||||
tags: [],
|
||||
},
|
||||
errors: [],
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export function overviewFiltersReducer(
|
||||
state = initialState,
|
||||
action: OverviewFiltersAction
|
||||
): OverviewFiltersState {
|
||||
switch (action.type) {
|
||||
case FETCH_OVERVIEW_FILTERS:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
};
|
||||
case FETCH_OVERVIEW_FILTERS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
filters: action.payload,
|
||||
loading: false,
|
||||
};
|
||||
case FETCH_OVERVIEW_FILTERS_FAIL:
|
||||
return {
|
||||
...state,
|
||||
errors: [...state.errors, action.payload],
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import {
|
|||
PopoverState,
|
||||
toggleIntegrationsPopover,
|
||||
setBasePath,
|
||||
setEsKueryString,
|
||||
triggerAppRefresh,
|
||||
UiPayload,
|
||||
} from '../actions/ui';
|
||||
|
@ -16,12 +17,14 @@ import {
|
|||
export interface UiState {
|
||||
integrationsPopoverOpen: PopoverState | null;
|
||||
basePath: string;
|
||||
esKuery: string;
|
||||
lastRefresh: number;
|
||||
}
|
||||
|
||||
const initialState: UiState = {
|
||||
integrationsPopoverOpen: null,
|
||||
basePath: '',
|
||||
esKuery: '',
|
||||
lastRefresh: Date.now(),
|
||||
};
|
||||
|
||||
|
@ -41,6 +44,11 @@ export const uiReducer = handleActions<UiState, UiPayload>(
|
|||
...state,
|
||||
lastRefresh: action.payload as number,
|
||||
}),
|
||||
|
||||
[String(setEsKueryString)]: (state, action: Action<string>) => ({
|
||||
...state,
|
||||
esKuery: action.payload as string,
|
||||
}),
|
||||
},
|
||||
initialState
|
||||
);
|
||||
|
|
|
@ -9,6 +9,16 @@ import { AppState } from '../../../state';
|
|||
|
||||
describe('state selectors', () => {
|
||||
const state: AppState = {
|
||||
overviewFilters: {
|
||||
filters: {
|
||||
locations: [],
|
||||
ports: [],
|
||||
schemes: [],
|
||||
tags: [],
|
||||
},
|
||||
errors: [],
|
||||
loading: false,
|
||||
},
|
||||
monitor: {
|
||||
monitorDetailsList: [],
|
||||
monitorLocationsList: new Map(),
|
||||
|
@ -24,7 +34,12 @@ describe('state selectors', () => {
|
|||
errors: [],
|
||||
loading: false,
|
||||
},
|
||||
ui: { basePath: 'yyz', integrationsPopoverOpen: null, lastRefresh: 125 },
|
||||
ui: {
|
||||
basePath: 'yyz',
|
||||
esKuery: '',
|
||||
integrationsPopoverOpen: null,
|
||||
lastRefresh: 125,
|
||||
},
|
||||
monitorStatus: {
|
||||
status: null,
|
||||
monitor: null,
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { UMGqlRange } from '../../../common/domain_types';
|
||||
import { UMResolver } from '../../../common/graphql/resolver_types';
|
||||
import {
|
||||
FilterBar,
|
||||
GetFilterBarQueryArgs,
|
||||
GetMonitorChartsDataQueryArgs,
|
||||
MonitorChart,
|
||||
|
@ -46,7 +45,6 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = (
|
|||
Query: {
|
||||
getSnapshotHistogram: UMGetSnapshotHistogram;
|
||||
getMonitorChartsData: UMGetMonitorChartsResolver;
|
||||
getFilterBar: UMGetFilterBarResolver;
|
||||
};
|
||||
} => ({
|
||||
Query: {
|
||||
|
@ -77,16 +75,5 @@ export const createMonitorsResolvers: CreateUMGraphQLResolvers = (
|
|||
location,
|
||||
});
|
||||
},
|
||||
async getFilterBar(
|
||||
_resolver,
|
||||
{ dateRangeStart, dateRangeEnd },
|
||||
{ APICaller }
|
||||
): Promise<FilterBar> {
|
||||
return await libs.monitors.getFilterBar({
|
||||
callES: APICaller,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,22 +7,6 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export const monitorsSchema = gql`
|
||||
"The data used to enrich the filter bar."
|
||||
type FilterBar {
|
||||
"A series of monitor IDs in the heartbeat indices."
|
||||
ids: [String!]
|
||||
"The location values users have configured for the agents."
|
||||
locations: [String!]
|
||||
"The ports of the monitored endpoints."
|
||||
ports: [Int!]
|
||||
"The schemes used by the monitors."
|
||||
schemes: [String!]
|
||||
"The possible status values contained in the indices."
|
||||
statuses: [String!]
|
||||
"The list of URLs"
|
||||
urls: [String!]
|
||||
}
|
||||
|
||||
type HistogramDataPoint {
|
||||
upCount: Int
|
||||
downCount: Int
|
||||
|
@ -136,19 +120,5 @@ export const monitorsSchema = gql`
|
|||
dateRangeEnd: String!
|
||||
location: String
|
||||
): MonitorChart
|
||||
|
||||
"Fetch the most recent event data for a monitor ID, date range, location."
|
||||
getLatestMonitors(
|
||||
"The lower limit of the date range."
|
||||
dateRangeStart: String!
|
||||
"The upper limit of the date range."
|
||||
dateRangeEnd: String!
|
||||
"Optional: a specific monitor ID filter."
|
||||
monitorId: String
|
||||
"Optional: a specific instance location filter."
|
||||
location: String
|
||||
): [Ping!]!
|
||||
|
||||
getFilterBar(dateRangeStart: String!, dateRangeEnd: String!): FilterBar
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`extractFilterAggsResults extracts the bucket values of the expected filter fields 1`] = `
|
||||
Object {
|
||||
"locations": Array [
|
||||
"us-east-2",
|
||||
"fairbanks",
|
||||
],
|
||||
"ports": Array [
|
||||
12349,
|
||||
80,
|
||||
5601,
|
||||
8200,
|
||||
9200,
|
||||
9292,
|
||||
],
|
||||
"schemes": Array [
|
||||
"http",
|
||||
"tcp",
|
||||
"icmp",
|
||||
],
|
||||
"tags": Array [
|
||||
"api",
|
||||
"dev",
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,171 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`generateFilterAggs generates expected aggregations object 1`] = `
|
||||
Object {
|
||||
"locations": Object {
|
||||
"aggs": Object {
|
||||
"term": Object {
|
||||
"terms": Object {
|
||||
"field": "observer.geo.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"bool": Object {
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"url.port": "80",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"url.port": "5601",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"tags": "api",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"monitor.type": "http",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"monitor.type": "tcp",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"ports": Object {
|
||||
"aggs": Object {
|
||||
"term": Object {
|
||||
"terms": Object {
|
||||
"field": "url.port",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"bool": Object {
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"observer.geo.name": "fairbanks",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"observer.geo.name": "us-east-2",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"tags": "api",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"monitor.type": "http",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"monitor.type": "tcp",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"schemes": Object {
|
||||
"aggs": Object {
|
||||
"term": Object {
|
||||
"terms": Object {
|
||||
"field": "monitor.type",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"bool": Object {
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"observer.geo.name": "fairbanks",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"observer.geo.name": "us-east-2",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"url.port": "80",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"url.port": "5601",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"tags": "api",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"tags": Object {
|
||||
"aggs": Object {
|
||||
"term": Object {
|
||||
"terms": Object {
|
||||
"field": "tags",
|
||||
},
|
||||
},
|
||||
},
|
||||
"filter": Object {
|
||||
"bool": Object {
|
||||
"should": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"observer.geo.name": "fairbanks",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"observer.geo.name": "us-east-2",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"url.port": "80",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"url.port": "5601",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"monitor.type": "http",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"monitor.type": "tcp",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { combineRangeWithFilters } from '../elasticsearch_monitors_adapter';
|
||||
|
||||
describe('combineRangeWithFilters', () => {
|
||||
it('combines filters that have no filter clause', () => {
|
||||
expect(
|
||||
combineRangeWithFilters('now-15m', 'now', {
|
||||
bool: { should: [{ match: { 'url.port': 80 } }], minimum_should_match: 1 },
|
||||
})
|
||||
).toEqual({
|
||||
bool: {
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'url.port': 80,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-15m',
|
||||
lte: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('combines query with filter object', () => {
|
||||
expect(
|
||||
combineRangeWithFilters('now-15m', 'now', {
|
||||
bool: {
|
||||
filter: { term: { field: 'monitor.id' } },
|
||||
should: [{ match: { 'url.port': 80 } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
field: 'monitor.id',
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-15m',
|
||||
lte: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'url.port': 80,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('combines query with filter list', () => {
|
||||
expect(
|
||||
combineRangeWithFilters('now-15m', 'now', {
|
||||
bool: {
|
||||
filter: [{ field: 'monitor.id' }],
|
||||
should: [{ match: { 'url.port': 80 } }],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
field: 'monitor.id',
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 'now-15m',
|
||||
lte: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
should: [
|
||||
{
|
||||
match: {
|
||||
'url.port': 80,
|
||||
},
|
||||
},
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { extractFilterAggsResults } from '../elasticsearch_monitors_adapter';
|
||||
|
||||
describe('extractFilterAggsResults', () => {
|
||||
it('extracts the bucket values of the expected filter fields', () => {
|
||||
expect(
|
||||
extractFilterAggsResults(
|
||||
{
|
||||
locations: {
|
||||
doc_count: 8098,
|
||||
term: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{ key: 'us-east-2', doc_count: 4050 },
|
||||
{ key: 'fairbanks', doc_count: 4048 },
|
||||
],
|
||||
},
|
||||
},
|
||||
schemes: {
|
||||
doc_count: 8098,
|
||||
term: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{ key: 'http', doc_count: 5055 },
|
||||
{ key: 'tcp', doc_count: 2685 },
|
||||
{ key: 'icmp', doc_count: 358 },
|
||||
],
|
||||
},
|
||||
},
|
||||
ports: {
|
||||
doc_count: 8098,
|
||||
term: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{ key: 12349, doc_count: 3571 },
|
||||
{ key: 80, doc_count: 2985 },
|
||||
{ key: 5601, doc_count: 358 },
|
||||
{ key: 8200, doc_count: 358 },
|
||||
{ key: 9200, doc_count: 358 },
|
||||
{ key: 9292, doc_count: 110 },
|
||||
],
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
doc_count: 8098,
|
||||
term: {
|
||||
doc_count_error_upper_bound: 0,
|
||||
sum_other_doc_count: 0,
|
||||
buckets: [
|
||||
{ key: 'api', doc_count: 8098 },
|
||||
{ key: 'dev', doc_count: 8098 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
['locations', 'ports', 'schemes', 'tags']
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { generateFilterAggs } from '../generate_filter_aggs';
|
||||
|
||||
describe('generateFilterAggs', () => {
|
||||
it('generates expected aggregations object', () => {
|
||||
expect(
|
||||
generateFilterAggs(
|
||||
[
|
||||
{ aggName: 'locations', filterName: 'locations', field: 'observer.geo.name' },
|
||||
{ aggName: 'ports', filterName: 'ports', field: 'url.port' },
|
||||
{ aggName: 'schemes', filterName: 'schemes', field: 'monitor.type' },
|
||||
{ aggName: 'tags', filterName: 'tags', field: 'tags' },
|
||||
],
|
||||
{
|
||||
locations: ['fairbanks', 'us-east-2'],
|
||||
ports: ['80', '5601'],
|
||||
tags: ['api'],
|
||||
schemes: ['http', 'tcp'],
|
||||
}
|
||||
)
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -6,7 +6,11 @@
|
|||
|
||||
import { MonitorChart } from '../../../../common/graphql/types';
|
||||
import { UMElasticsearchQueryFn } from '../framework';
|
||||
import { MonitorDetails, MonitorLocations } from '../../../../common/runtime_types';
|
||||
import {
|
||||
MonitorDetails,
|
||||
MonitorLocations,
|
||||
OverviewFilters,
|
||||
} from '../../../../common/runtime_types';
|
||||
|
||||
export interface GetMonitorChartsDataParams {
|
||||
/** @member monitorId ID value for the selected monitor */
|
||||
|
@ -20,9 +24,15 @@ export interface GetMonitorChartsDataParams {
|
|||
}
|
||||
|
||||
export interface GetFilterBarParams {
|
||||
/** @param dateRangeStart timestamp bounds */
|
||||
dateRangeStart: string;
|
||||
/** @member dateRangeEnd timestamp bounds */
|
||||
dateRangeEnd: string;
|
||||
/** @member search this value should correspond to Elasticsearch DSL
|
||||
* generated from KQL text the user provided.
|
||||
*/
|
||||
search?: Record<string, any>;
|
||||
filterOptions: Record<string, string[] | number[]>;
|
||||
}
|
||||
|
||||
export interface GetMonitorDetailsParams {
|
||||
|
@ -48,10 +58,13 @@ export interface UMMonitorsAdapter {
|
|||
* Fetches data used to populate monitor charts
|
||||
*/
|
||||
getMonitorChartsData: UMElasticsearchQueryFn<GetMonitorChartsDataParams, MonitorChart>;
|
||||
getFilterBar: UMElasticsearchQueryFn<GetFilterBarParams, any>;
|
||||
|
||||
/**
|
||||
* Fetch data for the monitor page title.
|
||||
* Fetch options for the filter bar.
|
||||
*/
|
||||
getFilterBar: UMElasticsearchQueryFn<GetFilterBarParams, OverviewFilters>;
|
||||
|
||||
getMonitorDetails: UMElasticsearchQueryFn<GetMonitorDetailsParams, MonitorDetails>;
|
||||
|
||||
getMonitorLocations: UMElasticsearchQueryFn<GetMonitorLocationsParams, MonitorLocations>;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,52 @@ import { MonitorChart, LocationDurationLine } from '../../../../common/graphql/t
|
|||
import { getHistogramIntervalFormatted } from '../../helper';
|
||||
import { MonitorError, MonitorLocation } from '../../../../common/runtime_types';
|
||||
import { UMMonitorsAdapter } from './adapter_types';
|
||||
import { generateFilterAggs } from './generate_filter_aggs';
|
||||
import { OverviewFilters } from '../../../../common/runtime_types';
|
||||
|
||||
export const combineRangeWithFilters = (
|
||||
dateRangeStart: string,
|
||||
dateRangeEnd: string,
|
||||
filters?: Record<string, any>
|
||||
) => {
|
||||
const range = {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: dateRangeStart,
|
||||
lte: dateRangeEnd,
|
||||
},
|
||||
},
|
||||
};
|
||||
if (!filters) return range;
|
||||
const clientFiltersList = Array.isArray(filters?.bool?.filter ?? {})
|
||||
? // i.e. {"bool":{"filter":{ ...some nested filter objects }}}
|
||||
filters.bool.filter
|
||||
: // i.e. {"bool":{"filter":[ ...some listed filter objects ]}}
|
||||
Object.keys(filters?.bool?.filter ?? {}).map(key => ({
|
||||
...filters?.bool?.filter?.[key],
|
||||
}));
|
||||
filters.bool.filter = [...clientFiltersList, range];
|
||||
return filters;
|
||||
};
|
||||
|
||||
type SupportedFields = 'locations' | 'ports' | 'schemes' | 'tags';
|
||||
|
||||
export const extractFilterAggsResults = (
|
||||
responseAggregations: Record<string, any>,
|
||||
keys: SupportedFields[]
|
||||
): OverviewFilters => {
|
||||
const values: OverviewFilters = {
|
||||
locations: [],
|
||||
ports: [],
|
||||
schemes: [],
|
||||
tags: [],
|
||||
};
|
||||
keys.forEach(key => {
|
||||
const buckets = responseAggregations[key]?.term?.buckets ?? [];
|
||||
values[key] = buckets.map((item: { key: string | number }) => item.key);
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
const formatStatusBuckets = (time: any, buckets: any, docCount: any) => {
|
||||
let up = null;
|
||||
|
@ -160,39 +206,30 @@ export const elasticsearchMonitorsAdapter: UMMonitorsAdapter = {
|
|||
return monitorChartsData;
|
||||
},
|
||||
|
||||
getFilterBar: async ({ callES, dateRangeStart, dateRangeEnd }) => {
|
||||
const fields: { [key: string]: string } = {
|
||||
ids: 'monitor.id',
|
||||
schemes: 'monitor.type',
|
||||
urls: 'url.full',
|
||||
ports: 'url.port',
|
||||
locations: 'observer.geo.name',
|
||||
};
|
||||
getFilterBar: async ({ callES, dateRangeStart, dateRangeEnd, search, filterOptions }) => {
|
||||
const aggs = generateFilterAggs(
|
||||
[
|
||||
{ aggName: 'locations', filterName: 'locations', field: 'observer.geo.name' },
|
||||
{ aggName: 'ports', filterName: 'ports', field: 'url.port' },
|
||||
{ aggName: 'schemes', filterName: 'schemes', field: 'monitor.type' },
|
||||
{ aggName: 'tags', filterName: 'tags', field: 'tags' },
|
||||
],
|
||||
filterOptions
|
||||
);
|
||||
const filters = combineRangeWithFilters(dateRangeStart, dateRangeEnd, search);
|
||||
const params = {
|
||||
index: INDEX_NAMES.HEARTBEAT,
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: dateRangeStart,
|
||||
lte: dateRangeEnd,
|
||||
},
|
||||
},
|
||||
...filters,
|
||||
},
|
||||
aggs: Object.values(fields).reduce((acc: { [key: string]: any }, field) => {
|
||||
acc[field] = { terms: { field, size: 20 } };
|
||||
return acc;
|
||||
}, {}),
|
||||
aggs,
|
||||
},
|
||||
};
|
||||
const { aggregations } = await callES('search', params);
|
||||
|
||||
return Object.keys(fields).reduce((acc: { [key: string]: any[] }, field) => {
|
||||
const bucketName = fields[field];
|
||||
acc[field] = aggregations[bucketName].buckets.map((b: { key: string | number }) => b.key);
|
||||
return acc;
|
||||
}, {});
|
||||
const { aggregations } = await callES('search', params);
|
||||
return extractFilterAggsResults(aggregations, ['tags', 'locations', 'ports', 'schemes']);
|
||||
},
|
||||
|
||||
getMonitorDetails: async ({ callES, monitorId, dateStart, dateEnd }) => {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
interface AggDefinition {
|
||||
aggName: string;
|
||||
filterName: string;
|
||||
field: string;
|
||||
}
|
||||
|
||||
export const FIELD_MAPPINGS: Record<string, string> = {
|
||||
schemes: 'monitor.type',
|
||||
ports: 'url.port',
|
||||
locations: 'observer.geo.name',
|
||||
tags: 'tags',
|
||||
};
|
||||
|
||||
const getFilterAggConditions = (filterTerms: Record<string, any[]>, except: string) => {
|
||||
const filters: any[] = [];
|
||||
|
||||
Object.keys(filterTerms).forEach((key: string) => {
|
||||
if (key === except && FIELD_MAPPINGS[key]) return;
|
||||
filters.push(
|
||||
...filterTerms[key].map(value => ({
|
||||
term: {
|
||||
[FIELD_MAPPINGS[key]]: value,
|
||||
},
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
return filters;
|
||||
};
|
||||
|
||||
export const generateFilterAggs = (
|
||||
aggDefinitions: AggDefinition[],
|
||||
filterOptions: Record<string, string[] | number[]>
|
||||
) =>
|
||||
aggDefinitions
|
||||
.map(({ aggName, filterName, field }) => ({
|
||||
[aggName]: {
|
||||
filter: {
|
||||
bool: {
|
||||
should: [...getFilterAggConditions(filterOptions, filterName)],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
term: {
|
||||
terms: {
|
||||
field,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.reduce((parent: Record<string, any>, agg: any) => ({ ...parent, ...agg }), {});
|
|
@ -9,3 +9,4 @@ export { getHistogramInterval } from './get_histogram_interval';
|
|||
export { getHistogramIntervalFormatted } from './get_histogram_interval_formatted';
|
||||
export { parseFilterQuery } from './parse_filter_query';
|
||||
export { assertCloseTo } from './assert_close_to';
|
||||
export { objectValuesToArrays } from './object_to_array';
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts the top-level fields of an object from an object to an array.
|
||||
* @param record the obect to map
|
||||
* @type T the type of the objects/arrays that will be mapped
|
||||
*/
|
||||
export const objectValuesToArrays = <T>(record: Record<string, T | T[]>): Record<string, T[]> => {
|
||||
const obj: Record<string, T[]> = {};
|
||||
Object.keys(record).forEach((key: string) => {
|
||||
const value = record[key];
|
||||
obj[key] = value ? (Array.isArray(value) ? value : [value]) : [];
|
||||
});
|
||||
return obj;
|
||||
};
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { createGetOverviewFilters } from './overview_filters';
|
||||
import { createGetAllRoute } from './pings';
|
||||
import { createGetIndexPatternRoute } from './index_pattern';
|
||||
import { createLogMonitorPageRoute, createLogOverviewPageRoute } from './telemetry';
|
||||
|
@ -20,6 +21,7 @@ export * from './types';
|
|||
export { createRouteWithAuth } from './create_route_with_auth';
|
||||
export { uptimeRouteWrapper } from './uptime_route_wrapper';
|
||||
export const restApiRoutes: UMRestApiRouteFactory[] = [
|
||||
createGetOverviewFilters,
|
||||
createGetAllRoute,
|
||||
createGetIndexPatternRoute,
|
||||
createGetMonitorRoute,
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { UMServerLibs } from '../../lib/lib';
|
||||
import { UMRestApiRouteFactory } from '../types';
|
||||
import { objectValuesToArrays } from '../../lib/helper';
|
||||
|
||||
const arrayOrStringType = schema.maybe(
|
||||
schema.oneOf([schema.string(), schema.arrayOf(schema.string())])
|
||||
);
|
||||
|
||||
export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLibs) => ({
|
||||
method: 'GET',
|
||||
path: '/api/uptime/filters',
|
||||
validate: {
|
||||
query: schema.object({
|
||||
dateRangeStart: schema.string(),
|
||||
dateRangeEnd: schema.string(),
|
||||
search: schema.maybe(schema.string()),
|
||||
locations: arrayOrStringType,
|
||||
schemes: arrayOrStringType,
|
||||
ports: arrayOrStringType,
|
||||
tags: arrayOrStringType,
|
||||
}),
|
||||
},
|
||||
|
||||
options: {
|
||||
tags: ['access:uptime'],
|
||||
},
|
||||
handler: async ({ callES }, _context, request, response) => {
|
||||
const { dateRangeStart, dateRangeEnd, locations, schemes, search, ports, tags } = request.query;
|
||||
|
||||
let parsedSearch: Record<string, any> | undefined;
|
||||
if (search) {
|
||||
try {
|
||||
parsedSearch = JSON.parse(search);
|
||||
} catch (e) {
|
||||
return response.badRequest({ body: { message: e.message } });
|
||||
}
|
||||
}
|
||||
|
||||
const filtersResponse = await libs.monitors.getFilterBar({
|
||||
callES,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
search: parsedSearch,
|
||||
filterOptions: objectValuesToArrays<string>({
|
||||
locations,
|
||||
ports,
|
||||
schemes,
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
return response.ok({ body: { ...filtersResponse } });
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { createGetOverviewFilters } from './get_overview_filters';
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { expectFixtureEql } from './helpers/expect_fixture_eql';
|
||||
import { filterBarQueryString } from '../../../../../legacy/plugins/uptime/public/queries';
|
||||
|
||||
export default function({ getService }) {
|
||||
describe('filterBar query', () => {
|
||||
before('load heartbeat data', () => getService('esArchiver').load('uptime/full_heartbeat'));
|
||||
after('unload heartbeat index', () => getService('esArchiver').unload('uptime/full_heartbeat'));
|
||||
|
||||
const supertest = getService('supertest');
|
||||
|
||||
it('returns the expected filters', async () => {
|
||||
const getFilterBarQuery = {
|
||||
operationName: 'FilterBar',
|
||||
query: filterBarQueryString,
|
||||
variables: {
|
||||
dateRangeStart: '2019-01-28T17:40:08.078Z',
|
||||
dateRangeEnd: '2025-01-28T19:00:16.078Z',
|
||||
},
|
||||
};
|
||||
const {
|
||||
body: { data },
|
||||
} = await supertest
|
||||
.post('/api/uptime/graphql')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({ ...getFilterBarQuery });
|
||||
expectFixtureEql(data, 'filter_list');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"filterBar": {
|
||||
"ids": [
|
||||
"0000-intermittent",
|
||||
"0001-up",
|
||||
"0002-up",
|
||||
"0003-up",
|
||||
"0004-up",
|
||||
"0005-up",
|
||||
"0006-up",
|
||||
"0007-up",
|
||||
"0008-up",
|
||||
"0009-up",
|
||||
"0010-down",
|
||||
"0011-up",
|
||||
"0012-up",
|
||||
"0013-up",
|
||||
"0014-up",
|
||||
"0015-intermittent",
|
||||
"0016-up",
|
||||
"0017-up",
|
||||
"0018-up",
|
||||
"0019-up"
|
||||
],
|
||||
"locations": [
|
||||
"mpls"
|
||||
],
|
||||
"ports": [
|
||||
5678
|
||||
],
|
||||
"schemes": [
|
||||
"http"
|
||||
],
|
||||
"urls": [
|
||||
"http://localhost:5678/pattern?r=200x1",
|
||||
"http://localhost:5678/pattern?r=200x5,500x1",
|
||||
"http://localhost:5678/pattern?r=400x1"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"schemes": [
|
||||
"http"
|
||||
],
|
||||
"ports": [
|
||||
5678
|
||||
],
|
||||
"locations": [
|
||||
"mpls"
|
||||
],
|
||||
"tags": []
|
||||
}
|
|
@ -11,7 +11,6 @@ export default function({ loadTestFile }) {
|
|||
// verifying the pre-loaded documents are returned in a way that
|
||||
// matches the snapshots contained in './fixtures'
|
||||
loadTestFile(require.resolve('./doc_count'));
|
||||
loadTestFile(require.resolve('./filter_bar'));
|
||||
loadTestFile(require.resolve('./monitor_charts'));
|
||||
loadTestFile(require.resolve('./monitor_states'));
|
||||
loadTestFile(require.resolve('./ping_list'));
|
||||
|
|
27
x-pack/test/api_integration/apis/uptime/rest/filters.ts
Normal file
27
x-pack/test/api_integration/apis/uptime/rest/filters.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { expectFixtureEql } from '../graphql/helpers/expect_fixture_eql';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const getApiPath = (dateRangeStart: string, dateRangeEnd: string, filters?: string) =>
|
||||
`/api/uptime/filters?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}${
|
||||
filters ? `&filters=${filters}` : ''
|
||||
}`;
|
||||
|
||||
export default function({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
describe('filter group endpoint', () => {
|
||||
const dateRangeStart = '2019-01-28T17:40:08.078Z';
|
||||
const dateRangeEnd = '2025-01-28T19:00:16.078Z';
|
||||
|
||||
it('returns expected filters', async () => {
|
||||
const resp = await supertest.get(getApiPath(dateRangeStart, dateRangeEnd));
|
||||
expectFixtureEql(resp.body, 'filters');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -29,6 +29,27 @@ export default ({ getPageObjects }: FtrProviderContext) => {
|
|||
await pageObjects.uptime.pageHasExpectedIds(['0000-intermittent']);
|
||||
});
|
||||
|
||||
it('applies filters for multiple fields', async () => {
|
||||
await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END);
|
||||
await pageObjects.uptime.selectFilterItems({
|
||||
location: ['mpls'],
|
||||
port: ['5678'],
|
||||
scheme: ['http'],
|
||||
});
|
||||
await pageObjects.uptime.pageHasExpectedIds([
|
||||
'0000-intermittent',
|
||||
'0001-up',
|
||||
'0002-up',
|
||||
'0003-up',
|
||||
'0004-up',
|
||||
'0005-up',
|
||||
'0006-up',
|
||||
'0007-up',
|
||||
'0008-up',
|
||||
'0009-up',
|
||||
]);
|
||||
});
|
||||
|
||||
it('pagination is cleared when filter criteria changes', async () => {
|
||||
await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END);
|
||||
await pageObjects.uptime.changePage('next');
|
||||
|
|
|
@ -71,6 +71,17 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo
|
|||
}
|
||||
}
|
||||
|
||||
public async selectFilterItems(filters: Record<string, string[]>) {
|
||||
for (const key in filters) {
|
||||
if (filters.hasOwnProperty(key)) {
|
||||
const values = filters[key];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
await uptimeService.selectFilterItem(key, values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getSnapshotCount() {
|
||||
return await uptimeService.getSnapshotCount();
|
||||
}
|
||||
|
|
|
@ -49,6 +49,15 @@ export function UptimeProvider({ getService }: FtrProviderContext) {
|
|||
async setStatusFilterDown() {
|
||||
await testSubjects.click('xpack.uptime.filterBar.filterStatusDown');
|
||||
},
|
||||
async selectFilterItem(filterType: string, option: string) {
|
||||
const popoverId = `filter-popover_${filterType}`;
|
||||
const optionId = `filter-popover-item_${option}`;
|
||||
await testSubjects.existOrFail(popoverId);
|
||||
await testSubjects.click(popoverId);
|
||||
await testSubjects.existOrFail(optionId);
|
||||
await testSubjects.click(optionId);
|
||||
await testSubjects.click(popoverId);
|
||||
},
|
||||
async getSnapshotCount() {
|
||||
return {
|
||||
up: await testSubjects.getVisibleText('xpack.uptime.snapshot.donutChart.up'),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue