[Uptime] Edit uptime alerts (#68005)

* Extract store creation to plugin start, add redux providers to alert registration.

* Update unit test.

* Move alert registration to `setup` function.

* Allow external editing of uptime client alert types.

* Move alert initialization back to `start`.

* Clean up interfaces for alert types.

* Add code that will work for settings link even outside uptime app.

* Create new atomic params type for status alerts.

* Update executor params typing to support both alert params types.

* Update snapshot for alert factory function.

* Fix broken types and refresh snapshots.

* Allow edits of filters for monitor alerts.

* Support default parameter value for numTimes.

* Support default parameter values for timerange.

* Modify kuery bar to work for alert edits, fix some filter issues.

* Clean up tests and fix types.

* Fix types and add a test.

* Add callout and validation handling for old alerts while editing.

* Add a test for updated validation function.

* Define window for overview filters fetch action.

* Revert store initialization.

* Make monitor counter function while editing alerts.

* Refresh snapshot.

* Move snapshot count in monitor status alert to callout.

* Add new state for selected filters.

* Add basic functional tests for uptime alert flyouts.

* Fix broken types.

* Update unit tests with mock provider.

* Remove unneeded params from hook.

* Add more unit tests.

* Reducing functional test flakiness.

* Alert flyout controls update url only within Uptime app.

* Extract context interaction to container component, update snapshots.

* Add missing parameter to test file.

* Remove flaky functional test.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
Justin Kambic 2020-06-08 15:15:47 -04:00 committed by GitHub
parent 891342a76f
commit 858523eac6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1524 additions and 389 deletions

View file

@ -3,6 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import {
CoreSetup,
CoreStart,
@ -16,8 +17,16 @@ import { PLUGIN } from '../../common/constants';
import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public';
import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public';
import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public';
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public';
import {
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
} from '../../../triggers_actions_ui/public';
import {
DataPublicPluginSetup,
DataPublicPluginStart,
} from '../../../../../src/plugins/data/public';
import { alertTypeInitializers } from '../lib/alert_types';
import { kibanaService } from '../state/kibana_service';
export interface ClientPluginsSetup {
data: DataPublicPluginSetup;
@ -27,6 +36,8 @@ export interface ClientPluginsSetup {
export interface ClientPluginsStart {
embeddable: EmbeddableStart;
data: DataPublicPluginStart;
triggers_actions_ui: TriggersAndActionsUIPublicPluginStart;
}
export type ClientSetup = void;
@ -66,6 +77,7 @@ export class UptimePlugin
);
const { element } = params;
const libs: UMFrontendLibs = {
framework: getKibanaFrameworkAdapter(coreStart, plugins, corePlugins),
};
@ -74,7 +86,21 @@ export class UptimePlugin
});
}
public start(_start: CoreStart, _plugins: {}): void {}
public start(start: CoreStart, plugins: ClientPluginsStart): void {
kibanaService.core = start;
alertTypeInitializers.forEach((init) => {
const alertInitializer = init({
core: start,
plugins,
});
if (
plugins.triggers_actions_ui &&
!plugins.triggers_actions_ui.alertTypeRegistry.has(alertInitializer.id)
) {
plugins.triggers_actions_ui.alertTypeRegistry.register(alertInitializer);
}
});
}
public stop(): void {}
}

View file

@ -6,7 +6,7 @@
import React from 'react';
import { UptimeDatePicker } from '../uptime_date_picker';
import { renderWithRouter, shallowWithRouter } from '../../../lib';
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib';
describe('UptimeDatePicker component', () => {
it('validates props with shallow render', () => {
@ -15,7 +15,11 @@ describe('UptimeDatePicker component', () => {
});
it('renders properly with mock data', () => {
const component = renderWithRouter(<UptimeDatePicker />);
const component = renderWithRouter(
<MountWithReduxProvider>
<UptimeDatePicker />
</MountWithReduxProvider>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -6,7 +6,7 @@
import React from 'react';
import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series';
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../../lib';
import { HistogramPoint } from '../../../../../common/runtime_types';
describe('MonitorBarSeries component', () => {
@ -197,7 +197,11 @@ describe('MonitorBarSeries component', () => {
});
it('renders if the data series is present', () => {
const component = renderWithRouter(<MonitorBarSeries histogramSeries={histogramSeries} />);
const component = renderWithRouter(
<MountWithReduxProvider>
<MonitorBarSeries histogramSeries={histogramSeries} />
</MountWithReduxProvider>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -6,7 +6,7 @@
import React from 'react';
import { PingHistogramComponent, PingHistogramComponentProps } from '../ping_histogram';
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../../lib';
describe('PingHistogram component', () => {
const props: PingHistogramComponentProps = {
@ -49,7 +49,12 @@ describe('PingHistogram component', () => {
});
it('renders the component without errors', () => {
const component = renderWithRouter(<PingHistogramComponent {...props} />);
const component = renderWithRouter(
<MountWithReduxProvider>
<PingHistogramComponent {...props} />
</MountWithReduxProvider>
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,180 @@
/*
* 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 React from 'react';
import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers';
import { AddFilterButton } from '../add_filter_btn';
import { EuiButtonEmpty, EuiContextMenuItem } from '@elastic/eui';
describe('AddFilterButton component', () => {
it('provides all filter choices', () => {
const component = shallowWithIntl(
<AddFilterButton newFilters={[]} onNewFilter={jest.fn()} alertFilters={{}} />
);
expect(component).toMatchInlineSnapshot(`
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButtonEmpty
data-test-subj="uptimeCreateAlertAddFilter"
disabled={false}
iconType="plusInCircleFilled"
onClick={[Function]}
>
Add filter
</EuiButtonEmpty>
}
closePopover={[Function]}
display="inlineBlock"
hasArrow={true}
id="singlePanel"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
>
<EuiContextMenuPanel
hasFocus={true}
items={
Array [
<EuiContextMenuItem
data-test-subj="uptimeAlertAddFilter.observer.geo.name"
onClick={[Function]}
>
Location
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="uptimeAlertAddFilter.tags"
onClick={[Function]}
>
Tag
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="uptimeAlertAddFilter.url.port"
onClick={[Function]}
>
Port
</EuiContextMenuItem>,
<EuiContextMenuItem
data-test-subj="uptimeAlertAddFilter.monitor.type"
onClick={[Function]}
>
Type
</EuiContextMenuItem>,
]
}
/>
</EuiPopover>
`);
});
it('excludes filters that already have selected values', () => {
const component = shallowWithIntl(
<AddFilterButton
newFilters={['observer.geo.name', 'tags']}
alertFilters={{ 'url.port': ['443', '80'] }}
onNewFilter={jest.fn()}
/>
);
expect(component).toMatchInlineSnapshot(`
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButtonEmpty
data-test-subj="uptimeCreateAlertAddFilter"
disabled={false}
iconType="plusInCircleFilled"
onClick={[Function]}
>
Add filter
</EuiButtonEmpty>
}
closePopover={[Function]}
display="inlineBlock"
hasArrow={true}
id="singlePanel"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
>
<EuiContextMenuPanel
hasFocus={true}
items={
Array [
<EuiContextMenuItem
data-test-subj="uptimeAlertAddFilter.monitor.type"
onClick={[Function]}
>
Type
</EuiContextMenuItem>,
]
}
/>
</EuiPopover>
`);
});
it('popover is disabled if no values are available', () => {
const component = shallowWithIntl(
<AddFilterButton
newFilters={[]}
alertFilters={{
'observer.geo.name': ['fairbanks'],
tags: ['foo'],
'url.port': ['80'],
'monitor.type': ['http'],
}}
onNewFilter={jest.fn()}
/>
);
expect(component).toMatchInlineSnapshot(`
<EuiPopover
anchorPosition="downLeft"
button={
<EuiButtonEmpty
data-test-subj="uptimeCreateAlertAddFilter"
disabled={true}
iconType="plusInCircleFilled"
onClick={[Function]}
>
Add filter
</EuiButtonEmpty>
}
closePopover={[Function]}
display="inlineBlock"
hasArrow={true}
id="singlePanel"
isOpen={false}
ownFocus={false}
panelPaddingSize="none"
>
<EuiContextMenuPanel
hasFocus={true}
items={Array []}
/>
</EuiPopover>
`);
});
it('filter select', () => {
const mockOnNewFilter = jest.fn();
const component = mountWithIntl(
<AddFilterButton newFilters={[]} alertFilters={{}} onNewFilter={mockOnNewFilter} />
);
component.find(EuiButtonEmpty).simulate('click', { target: { value: '0' } });
component
.find(EuiContextMenuItem)
.first()
.simulate('click', { target: { value: '0' } });
expect(mockOnNewFilter).toHaveBeenCalled();
expect(mockOnNewFilter.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"observer.geo.name",
],
]
`);
});
});

View file

@ -0,0 +1,145 @@
/*
* 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 React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { AlertFieldNumber, handleAlertFieldNumberChange } from '../alert_field_number';
describe('AlertFieldNumber', () => {
describe('handleAlertFieldNumberChange', () => {
let mockSetIsInvalid: jest.Mock;
let mockSetFieldValue: jest.Mock;
beforeEach(() => {
mockSetIsInvalid = jest.fn();
mockSetFieldValue = jest.fn();
});
afterEach(() => {
jest.clearAllMocks();
});
it('sets a valid number', () => {
handleAlertFieldNumberChange(
// @ts-ignore no need to implement this entire type here
{ target: { value: '23' } },
false,
mockSetIsInvalid,
mockSetFieldValue
);
expect(mockSetIsInvalid).not.toHaveBeenCalled();
expect(mockSetFieldValue).toHaveBeenCalledTimes(1);
expect(mockSetFieldValue.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
23,
],
]
`);
});
it('sets invalid for NaN value', () => {
handleAlertFieldNumberChange(
// @ts-ignore no need to implement this entire type here
{ target: { value: 'foo' } },
false,
mockSetIsInvalid,
mockSetFieldValue
);
expect(mockSetIsInvalid).toHaveBeenCalledTimes(1);
expect(mockSetIsInvalid.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
true,
],
]
`);
expect(mockSetFieldValue).not.toHaveBeenCalled();
});
it('sets invalid to false when a valid value is received and invalid is true', () => {
handleAlertFieldNumberChange(
// @ts-ignore no need to implement this entire type here
{ target: { value: '23' } },
true,
mockSetIsInvalid,
mockSetFieldValue
);
expect(mockSetIsInvalid).toHaveBeenCalledTimes(1);
expect(mockSetIsInvalid.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
false,
],
]
`);
expect(mockSetFieldValue).toHaveBeenCalledTimes(1);
expect(mockSetFieldValue.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
23,
],
]
`);
});
});
describe('AlertFieldNumber', () => {
it('responds with correct number value when a valid number is specified', () => {
const mockValueHandler = jest.fn();
const component = mountWithIntl(
<AlertFieldNumber
aria-label="test label"
data-test-subj="foo"
disabled={false}
fieldValue={23}
setFieldValue={mockValueHandler}
/>
);
component.find('input').simulate('change', { target: { value: '45' } });
expect(mockValueHandler).toHaveBeenCalled();
expect(mockValueHandler.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
45,
],
]
`);
});
it('does not set an invalid number value', () => {
const mockValueHandler = jest.fn();
const component = mountWithIntl(
<AlertFieldNumber
aria-label="test label"
data-test-subj="foo"
disabled={false}
fieldValue={23}
setFieldValue={mockValueHandler}
/>
);
component.find('input').simulate('change', { target: { value: 'not a number' } });
expect(mockValueHandler).not.toHaveBeenCalled();
expect(mockValueHandler.mock.calls).toEqual([]);
});
it('does not set a number value less than 1', () => {
const mockValueHandler = jest.fn();
const component = mountWithIntl(
<AlertFieldNumber
aria-label="test label"
data-test-subj="foo"
disabled={false}
fieldValue={23}
setFieldValue={mockValueHandler}
/>
);
component.find('input').simulate('change', { target: { value: '0' } });
expect(mockValueHandler).not.toHaveBeenCalled();
expect(mockValueHandler.mock.calls).toEqual([]);
});
});
});

View file

@ -5,141 +5,120 @@
*/
import React from 'react';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { AlertFieldNumber, handleAlertFieldNumberChange } from '../alert_field_number';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { AlertMonitorStatusComponent, AlertMonitorStatusProps } from '../alert_monitor_status';
describe('alert monitor status component', () => {
describe('handleAlertFieldNumberChange', () => {
let mockSetIsInvalid: jest.Mock;
let mockSetFieldValue: jest.Mock;
describe('AlertMonitorStatus', () => {
const defaultProps: AlertMonitorStatusProps = {
alertParams: {
numTimes: 3,
search: 'monitor.id: foo',
timerangeUnit: 'h',
timerangeCount: 21,
},
autocomplete: {
addQuerySuggestionProvider: jest.fn(),
getQuerySuggestions: jest.fn(),
},
enabled: true,
hasFilters: false,
isOldAlert: true,
locations: [],
shouldUpdateUrl: false,
snapshotCount: 0,
snapshotLoading: false,
numTimes: 14,
setAlertParams: jest.fn(),
timerange: { from: 'now-12h', to: 'now' },
};
beforeEach(() => {
mockSetIsInvalid = jest.fn();
mockSetFieldValue = jest.fn();
});
afterEach(() => {
jest.clearAllMocks();
});
it('sets a valid number', () => {
handleAlertFieldNumberChange(
// @ts-ignore no need to implement this entire type here
{ target: { value: '23' } },
false,
mockSetIsInvalid,
mockSetFieldValue
);
expect(mockSetIsInvalid).not.toHaveBeenCalled();
expect(mockSetFieldValue).toHaveBeenCalledTimes(1);
expect(mockSetFieldValue.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
23,
],
]
it('passes default props to children', () => {
const component = shallowWithIntl(<AlertMonitorStatusComponent {...defaultProps} />);
expect(component).toMatchInlineSnapshot(`
<Fragment>
<OldAlertCallOut
isOldAlert={true}
/>
<EuiSpacer
size="m"
/>
<KueryBar
aria-label="Input that allows filtering criteria for the monitor status alert"
autocomplete={
Object {
"addQuerySuggestionProvider": [MockFunction],
"getQuerySuggestions": [MockFunction],
}
}
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
defaultKuery="monitor.id: foo"
shouldUpdateUrl={false}
updateDefaultKuery={[Function]}
/>
<EuiSpacer
size="s"
/>
<DownNoExpressionSelect
defaultNumTimes={3}
hasFilters={false}
setAlertParams={[MockFunction]}
/>
<EuiSpacer
size="xs"
/>
<TimeExpressionSelect
defaultTimerangeCount={21}
defaultTimerangeUnit="h"
setAlertParams={[MockFunction]}
/>
<EuiSpacer
size="xs"
/>
<FiltersExpressionSelectContainer
alertParams={
Object {
"numTimes": 3,
"search": "monitor.id: foo",
"timerangeCount": 21,
"timerangeUnit": "h",
}
}
newFilters={Array []}
onRemoveFilter={[Function]}
setAlertParams={[MockFunction]}
shouldUpdateUrl={false}
/>
<EuiSpacer
size="xs"
/>
<AddFilterButton
newFilters={Array []}
onNewFilter={[Function]}
/>
<EuiSpacer
size="m"
/>
<EuiCallOut
iconType="iInCircle"
size="s"
title={
<FormattedMessage
defaultMessage="This alert will apply to approximately {snapshotCount} monitors."
id="xpack.uptime.alerts.monitorStatus.monitorCallOut.title"
values={
Object {
"snapshotCount": 0,
}
}
/>
}
/>
<EuiSpacer
size="m"
/>
</Fragment>
`);
});
it('sets invalid for NaN value', () => {
handleAlertFieldNumberChange(
// @ts-ignore no need to implement this entire type here
{ target: { value: 'foo' } },
false,
mockSetIsInvalid,
mockSetFieldValue
);
expect(mockSetIsInvalid).toHaveBeenCalledTimes(1);
expect(mockSetIsInvalid.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
true,
],
]
`);
expect(mockSetFieldValue).not.toHaveBeenCalled();
});
it('sets invalid to false when a valid value is received and invalid is true', () => {
handleAlertFieldNumberChange(
// @ts-ignore no need to implement this entire type here
{ target: { value: '23' } },
true,
mockSetIsInvalid,
mockSetFieldValue
);
expect(mockSetIsInvalid).toHaveBeenCalledTimes(1);
expect(mockSetIsInvalid.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
false,
],
]
`);
expect(mockSetFieldValue).toHaveBeenCalledTimes(1);
expect(mockSetFieldValue.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
23,
],
]
`);
});
});
describe('AlertFieldNumber', () => {
it('responds with correct number value when a valid number is specified', () => {
const mockValueHandler = jest.fn();
const component = mountWithIntl(
<AlertFieldNumber
aria-label="test label"
data-test-subj="foo"
disabled={false}
fieldValue={23}
setFieldValue={mockValueHandler}
/>
);
component.find('input').simulate('change', { target: { value: '45' } });
expect(mockValueHandler).toHaveBeenCalled();
expect(mockValueHandler.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
45,
],
]
`);
});
it('does not set an invalid number value', () => {
const mockValueHandler = jest.fn();
const component = mountWithIntl(
<AlertFieldNumber
aria-label="test label"
data-test-subj="foo"
disabled={false}
fieldValue={23}
setFieldValue={mockValueHandler}
/>
);
component.find('input').simulate('change', { target: { value: 'not a number' } });
expect(mockValueHandler).not.toHaveBeenCalled();
expect(mockValueHandler.mock.calls).toEqual([]);
});
it('does not set a number value less than 1', () => {
const mockValueHandler = jest.fn();
const component = mountWithIntl(
<AlertFieldNumber
aria-label="test label"
data-test-subj="foo"
disabled={false}
fieldValue={23}
setFieldValue={mockValueHandler}
/>
);
component.find('input').simulate('change', { target: { value: '0' } });
expect(mockValueHandler).not.toHaveBeenCalled();
expect(mockValueHandler.mock.calls).toEqual([]);
});
});
});

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { OldAlertCallOut } from '../old_alert_call_out';
describe('OldAlertCallOut', () => {
it('returns null for new alert type', () => {
expect(shallowWithIntl(<OldAlertCallOut isOldAlert={false} />)).toEqual({});
});
it('renders the call out for old alerts', () => {
expect(shallowWithIntl(<OldAlertCallOut isOldAlert={true} />)).toMatchInlineSnapshot(`
<Fragment>
<EuiSpacer
size="m"
/>
<EuiCallOut
iconType="alert"
size="s"
title={
<FormattedMessage
defaultMessage="You are editing an older alert, some fields may not auto-populate."
id="xpack.uptime.alerts.monitorStatus.oldAlertCallout.title"
values={Object {}}
/>
}
/>
</Fragment>
`);
});
});

View file

@ -6,20 +6,18 @@
import React, { useState } from 'react';
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { useFilterUpdate } from '../../../hooks/use_filter_update';
import * as labels from './translations';
interface Props {
newFilters: string[];
onNewFilter: (val: string) => void;
alertFilters: { [key: string]: string[] };
}
export const AddFilterButton: React.FC<Props> = ({ newFilters, onNewFilter }) => {
export const AddFilterButton: React.FC<Props> = ({ newFilters, onNewFilter, alertFilters }) => {
const [isPopoverOpen, setPopover] = useState(false);
const { selectedFilters } = useFilterUpdate();
const getSelectedItems = (fieldName: string) => selectedFilters.get(fieldName) || [];
const getSelectedItems = (fieldName: string) => alertFilters?.[fieldName] ?? [];
const onButtonClick = () => {
setPopover(!isPopoverOpen);

View file

@ -5,25 +5,31 @@
*/
import React, { useState } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { DataPublicPluginSetup } from 'src/plugins/data/public';
import * as labels from './translations';
import {
DownNoExpressionSelect,
TimeExpressionSelect,
FiltersExpressionsSelect,
FiltersExpressionSelectContainer,
} from './monitor_expressions';
import { AddFilterButton } from './add_filter_btn';
import { OldAlertCallOut } from './old_alert_call_out';
import { KueryBar } from '..';
interface AlertMonitorStatusProps {
export interface AlertMonitorStatusProps {
alertParams: { [key: string]: any };
autocomplete: DataPublicPluginSetup['autocomplete'];
enabled: boolean;
filters: string;
hasFilters: boolean;
isOldAlert: boolean;
locations: string[];
snapshotCount: number;
snapshotLoading: boolean;
numTimes: number;
setAlertParams: (key: string, value: any) => void;
shouldUpdateUrl: boolean;
timerange: {
from: string;
to: string;
@ -31,42 +37,70 @@ interface AlertMonitorStatusProps {
}
export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (props) => {
const { filters, setAlertParams } = props;
const {
alertParams,
hasFilters,
isOldAlert,
setAlertParams,
shouldUpdateUrl,
snapshotCount,
snapshotLoading,
} = props;
const [newFilters, setNewFilters] = useState<string[]>([]);
const alertFilters = alertParams?.filters ?? {};
const [newFilters, setNewFilters] = useState<string[]>(
Object.keys(alertFilters).filter((f) => alertFilters[f].length)
);
return (
<>
<OldAlertCallOut isOldAlert={isOldAlert} />
<EuiSpacer size="m" />
<KueryBar
aria-label={labels.ALERT_KUERY_BAR_ARIA}
autocomplete={props.autocomplete}
defaultKuery={alertParams.search}
shouldUpdateUrl={shouldUpdateUrl}
updateDefaultKuery={(value: string) => setAlertParams('search', value)}
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
/>
<EuiSpacer size="s" />
<DownNoExpressionSelect filters={filters} setAlertParams={setAlertParams} />
<EuiSpacer size="xs" />
<TimeExpressionSelect setAlertParams={setAlertParams} />
<EuiSpacer size="xs" />
<FiltersExpressionsSelect
<DownNoExpressionSelect
defaultNumTimes={alertParams.numTimes}
hasFilters={hasFilters}
setAlertParams={setAlertParams}
/>
<EuiSpacer size="xs" />
<TimeExpressionSelect
defaultTimerangeUnit={alertParams.timerangeUnit}
defaultTimerangeCount={alertParams.timerangeCount}
setAlertParams={setAlertParams}
/>
<EuiSpacer size="xs" />
<FiltersExpressionSelectContainer
alertParams={alertParams}
newFilters={newFilters}
onRemoveFilter={(removeFiler) => {
if (newFilters.includes(removeFiler)) {
setNewFilters(newFilters.filter((item) => item !== removeFiler));
onRemoveFilter={(removeFilter: string) => {
if (newFilters.includes(removeFilter)) {
setNewFilters(newFilters.filter((item) => item !== removeFilter));
}
}}
setAlertParams={setAlertParams}
shouldUpdateUrl={shouldUpdateUrl}
/>
<EuiSpacer size="xs" />
<AddFilterButton
alertFilters={alertParams.filters}
newFilters={newFilters}
onNewFilter={(newFilter) => {
setNewFilters([...newFilters, newFilter]);
@ -74,6 +108,20 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (p
/>
<EuiSpacer size="m" />
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.monitorCallOut.title"
defaultMessage="This alert will apply to approximately {snapshotCount} monitors."
values={{ snapshotCount: snapshotLoading ? '...' : snapshotCount }}
/>
}
iconType="iInCircle"
/>
<EuiSpacer size="m" />
</>
);
};

View file

@ -4,13 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import React, { useMemo, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import { DataPublicPluginSetup } from 'src/plugins/data/public';
import { selectMonitorStatusAlert, searchTextSelector } from '../../../../state/selectors';
import { isRight } from 'fp-ts/lib/Either';
import {
selectMonitorStatusAlert,
overviewFiltersSelector,
snapshotDataSelector,
esKuerySelector,
selectedFiltersSelector,
} from '../../../../state/selectors';
import { AlertMonitorStatusComponent } from '../index';
import {
fetchOverviewFilters,
setSearchTextAction,
setEsKueryString,
getSnapshotCountAction,
} from '../../../../state/actions';
import { AtomicStatusCheckParamsType } from '../../../../../common/runtime_types';
import { useIndexPattern } from '../../kuery_bar/use_index_pattern';
import { useUpdateKueryString } from '../../../../hooks';
interface Props {
alertParams: { [key: string]: any };
autocomplete: DataPublicPluginSetup['autocomplete'];
enabled: boolean;
numTimes: number;
@ -27,22 +45,87 @@ export const AlertMonitorStatus: React.FC<Props> = ({
numTimes,
setAlertParams,
timerange,
alertParams,
}) => {
const { filters, locations } = useSelector(selectMonitorStatusAlert);
const searchText = useSelector(searchTextSelector);
const dispatch = useDispatch();
useEffect(() => {
setAlertParams('search', searchText);
}, [setAlertParams, searchText]);
dispatch(
fetchOverviewFilters({
dateRangeStart: 'now-24h',
dateRangeEnd: 'now',
locations: alertParams.filters?.['observer.geo.name'] ?? [],
ports: alertParams.filters?.['url.port'] ?? [],
tags: alertParams.filters?.tags ?? [],
schemes: alertParams.filters?.['monitor.type'] ?? [],
})
);
}, [alertParams, dispatch]);
const overviewFilters = useSelector(overviewFiltersSelector);
const { locations } = useSelector(selectMonitorStatusAlert);
useEffect(() => {
if (alertParams.search) {
dispatch(setSearchTextAction(alertParams.search));
}
}, [alertParams, dispatch]);
const { index_pattern: indexPattern } = useIndexPattern();
const { count, loading } = useSelector(snapshotDataSelector);
const esKuery = useSelector(esKuerySelector);
const [esFilters] = useUpdateKueryString(
indexPattern,
alertParams.search,
alertParams.filters === undefined || typeof alertParams.filters === 'string'
? ''
: JSON.stringify(Array.from(Object.entries(alertParams.filters)))
);
useEffect(() => {
dispatch(setEsKueryString(esFilters ?? ''));
}, [dispatch, esFilters]);
const isOldAlert = React.useMemo(
() => !isRight(AtomicStatusCheckParamsType.decode(alertParams)),
[alertParams]
);
useEffect(() => {
dispatch(
getSnapshotCountAction({ dateRangeStart: 'now-24h', dateRangeEnd: 'now', filters: esKuery })
);
}, [dispatch, esKuery]);
const selectedFilters = useSelector(selectedFiltersSelector);
useEffect(() => {
if (!alertParams.filters && selectedFilters !== null) {
setAlertParams('filters', {
// @ts-ignore
'url.port': selectedFilters?.ports ?? [],
// @ts-ignore
'observer.geo.name': selectedFilters?.locations ?? [],
// @ts-ignore
'monitor.type': selectedFilters?.schemes ?? [],
// @ts-ignore
tags: selectedFilters?.tags ?? [],
});
}
}, [alertParams, setAlertParams, selectedFilters]);
const { pathname } = useLocation();
const shouldUpdateUrl = useMemo(() => pathname.indexOf('app/uptime') !== -1, [pathname]);
return (
<AlertMonitorStatusComponent
alertParams={alertParams}
autocomplete={autocomplete}
enabled={enabled}
filters={filters}
hasFilters={!!overviewFilters?.filters}
isOldAlert={isOldAlert}
locations={locations}
numTimes={numTimes}
setAlertParams={setAlertParams}
shouldUpdateUrl={shouldUpdateUrl}
snapshotCount={count.total}
snapshotLoading={loading}
timerange={timerange}
/>
);

View file

@ -9,20 +9,16 @@ import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { DownNoExpressionSelect } from '../down_number_select';
describe('DownNoExpressionSelect component', () => {
const filters =
'"{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"US-West"}}],"minimum_should_match":1}},' +
'{"bool":{"should":[{"match":{"url.port":443}}],"minimum_should_match":1}}]}}"';
it('should shallow renders against props', function () {
const component = shallowWithIntl(
<DownNoExpressionSelect filters={filters} setAlertParams={jest.fn()} />
<DownNoExpressionSelect hasFilters={true} setAlertParams={jest.fn()} />
);
expect(component).toMatchSnapshot();
});
it('should renders against props', function () {
const component = renderWithIntl(
<DownNoExpressionSelect filters={filters} setAlertParams={jest.fn()} />
<DownNoExpressionSelect hasFilters={true} setAlertParams={jest.fn()} />
);
expect(component).toMatchSnapshot();
});

View file

@ -0,0 +1,176 @@
/*
* 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 React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { FiltersExpressionsSelect } from '../filters_expression_select';
describe('filters expression select component', () => {
it('is empty when no filters available', () => {
const component = shallowWithIntl(
<FiltersExpressionsSelect
alertParams={{}}
newFilters={[]}
onRemoveFilter={jest.fn()}
filters={{
locations: [],
ports: [],
schemes: [],
tags: [],
}}
setAlertParams={jest.fn()}
setUpdatedFieldValues={jest.fn()}
shouldUpdateUrl={false}
/>
);
expect(component).toMatchInlineSnapshot(`
<Fragment>
<EuiSpacer
size="xs"
/>
</Fragment>
`);
});
it('contains provided new filter values', () => {
const component = shallowWithIntl(
<FiltersExpressionsSelect
alertParams={{}}
newFilters={['observer.geo.name']}
onRemoveFilter={jest.fn()}
filters={{
tags: [],
ports: [],
schemes: [],
locations: [],
}}
setAlertParams={jest.fn()}
setUpdatedFieldValues={jest.fn()}
shouldUpdateUrl={false}
/>
);
expect(component).toMatchInlineSnapshot(`
<Fragment>
<EuiFlexGroup
key="filter_location"
>
<EuiFlexItem>
<FilterPopover
btnContent={
<EuiExpression
aria-label="ariaLabel"
color="secondary"
data-test-subj="uptimeCreateStatusAlert.filter_location"
description="From"
onClick={[Function]}
value="any location"
/>
}
disabled={true}
fieldName="observer.geo.name"
forceOpen={false}
id="filter_location"
items={Array []}
loading={false}
onFilterFieldChange={[Function]}
selectedItems={Array []}
setForceOpen={[Function]}
title="Scheme"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Remove filter"
color="danger"
iconType="trash"
onClick={[Function]}
/>
</EuiFlexItem>
<EuiSpacer
size="xs"
/>
</EuiFlexGroup>
<EuiSpacer
size="xs"
/>
</Fragment>
`);
});
it('contains provided selected filter values', () => {
const component = shallowWithIntl(
<FiltersExpressionsSelect
alertParams={{}}
newFilters={['tags']}
onRemoveFilter={jest.fn()}
filters={{
tags: ['foo', 'bar'],
ports: [],
schemes: [],
locations: [],
}}
setAlertParams={jest.fn()}
setUpdatedFieldValues={jest.fn()}
shouldUpdateUrl={false}
/>
);
expect(component).toMatchInlineSnapshot(`
<Fragment>
<EuiFlexGroup
key="filter_tags"
>
<EuiFlexItem>
<FilterPopover
btnContent={
<EuiExpression
aria-label="ariaLabel"
color="secondary"
data-test-subj="uptimeCreateStatusAlert.filter_tags"
description="Using"
onClick={[Function]}
value="any tag"
/>
}
disabled={false}
fieldName="tags"
forceOpen={false}
id="filter_tags"
items={
Array [
"foo",
"bar",
]
}
loading={false}
onFilterFieldChange={[Function]}
selectedItems={Array []}
setForceOpen={[Function]}
title="Tags"
/>
</EuiFlexItem>
<EuiFlexItem
grow={false}
>
<EuiButtonIcon
aria-label="Remove filter"
color="danger"
iconType="trash"
onClick={[Function]}
/>
</EuiFlexItem>
<EuiSpacer
size="xs"
/>
</EuiFlexGroup>
<EuiSpacer
size="xs"
/>
</Fragment>
`);
});
});

View file

@ -10,12 +10,17 @@ import * as labels from '../translations';
import { AlertFieldNumber } from '../alert_field_number';
interface Props {
defaultNumTimes?: number;
hasFilters: boolean;
setAlertParams: (key: string, value: any) => void;
filters: string;
}
export const DownNoExpressionSelect: React.FC<Props> = ({ filters, setAlertParams }) => {
const [numTimes, setNumTimes] = useState<number>(5);
export const DownNoExpressionSelect: React.FC<Props> = ({
defaultNumTimes,
hasFilters,
setAlertParams,
}) => {
const [numTimes, setNumTimes] = useState<number>(defaultNumTimes ?? 5);
useEffect(() => {
setAlertParams('numTimes', numTimes);
@ -34,7 +39,7 @@ export const DownNoExpressionSelect: React.FC<Props> = ({ filters, setAlertParam
/>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression"
description={filters ? labels.MATCHING_MONITORS_DOWN : labels.ANY_MONITOR_DOWN}
description={hasFilters ? labels.MATCHING_MONITORS_DOWN : labels.ANY_MONITOR_DOWN}
id="ping-count"
value={`${numTimes} times`}
/>

View file

@ -4,57 +4,55 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import React, { useState } from 'react';
import { EuiButtonIcon, EuiExpression, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FilterPopover } from '../../filter_group/filter_popover';
import { overviewFiltersSelector } from '../../../../state/selectors';
import { useFilterUpdate } from '../../../../hooks/use_filter_update';
import { filterLabels } from '../../filter_group/translations';
import { alertFilterLabels } from './translations';
import { StatusCheckFilters } from '../../../../../common/runtime_types';
import { FilterExpressionsSelectProps } from './filters_expression_select_container';
import { OverviewFiltersState } from '../../../../state/reducers/overview_filters';
interface Props {
newFilters: string[];
onRemoveFilter: (val: string) => void;
setAlertParams: (key: string, value: any) => void;
type FilterFieldUpdate = (updateTarget: { fieldName: string; values: string[] }) => void;
interface OwnProps {
setUpdatedFieldValues: FilterFieldUpdate;
}
type Props = FilterExpressionsSelectProps & Pick<OverviewFiltersState, 'filters'> & OwnProps;
export const FiltersExpressionsSelect: React.FC<Props> = ({
setAlertParams,
alertParams,
filters: overviewFilters,
newFilters,
onRemoveFilter,
setAlertParams,
setUpdatedFieldValues,
}) => {
const {
filters: { tags, ports, schemes, locations },
} = useSelector(overviewFiltersSelector);
const [updatedFieldValues, setUpdatedFieldValues] = useState<{
fieldName: string;
values: string[];
}>({ fieldName: '', values: [] });
const { selectedLocations, selectedPorts, selectedSchemes, selectedTags } = useFilterUpdate(
updatedFieldValues.fieldName,
updatedFieldValues.values
);
const [filters, setFilters] = useState<StatusCheckFilters>({
'observer.geo.name': selectedLocations,
'url.port': selectedPorts,
tags: selectedTags,
'monitor.type': selectedSchemes,
});
useEffect(() => {
setAlertParams('filters', filters);
}, [filters, setAlertParams]);
const { tags, ports, schemes, locations } = overviewFilters;
const selectedPorts = alertParams?.filters?.['url.port'] ?? [];
const selectedLocations = alertParams?.filters?.['observer.geo.name'] ?? [];
const selectedSchemes = alertParams?.filters?.['monitor.type'] ?? [];
const selectedTags = alertParams?.filters?.tags ?? [];
const onFilterFieldChange = (fieldName: string, values: string[]) => {
setFilters({
...filters,
[fieldName]: values,
});
// the `filters` field is no longer a string
if (alertParams.filters && typeof alertParams.filters !== 'string') {
setAlertParams('filters', { ...alertParams.filters, [fieldName]: values });
} else {
setAlertParams(
'filters',
Object.assign(
{},
{
tags: [],
'url.port': [],
'observer.geo.name': [],
'monitor.type': [],
},
{ [fieldName]: values }
)
);
}
setUpdatedFieldValues({ fieldName, values });
};

View file

@ -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.
*/
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { FiltersExpressionsSelect } from './filters_expression_select';
import { overviewFiltersSelector } from '../../../../state/selectors';
import { useFilterUpdate } from '../../../../hooks/use_filter_update';
export interface FilterExpressionsSelectProps {
alertParams: { [key: string]: any };
newFilters: string[];
onRemoveFilter: (val: string) => void;
setAlertParams: (key: string, value: any) => void;
shouldUpdateUrl: boolean;
}
export const FiltersExpressionSelectContainer: React.FC<FilterExpressionsSelectProps> = (props) => {
const [updatedFieldValues, setUpdatedFieldValues] = useState<{
fieldName: string;
values: string[];
}>({ fieldName: '', values: [] });
useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values, props.shouldUpdateUrl);
const overviewFilters = useSelector(overviewFiltersSelector);
return (
<FiltersExpressionsSelect
{...overviewFilters}
{...props}
setUpdatedFieldValues={setUpdatedFieldValues}
/>
);
};

View file

@ -6,4 +6,5 @@
export { DownNoExpressionSelect } from './down_number_select';
export { FiltersExpressionsSelect } from './filters_expression_select';
export { FiltersExpressionSelectContainer } from './filters_expression_select_container';
export { TimeExpressionSelect } from './time_expression_select';

View file

@ -13,9 +13,13 @@ import { AlertFieldNumber } from '../alert_field_number';
import { timeExpLabels } from './translations';
interface Props {
defaultTimerangeCount?: number;
defaultTimerangeUnit?: string;
setAlertParams: (key: string, value: any) => void;
}
const DEFAULT_TIMERANGE_UNIT = 'm';
const TimeRangeOptions = [
{
'aria-label': labels.SECONDS_TIME_RANGE,
@ -26,7 +30,6 @@ const TimeRangeOptions = [
{
'aria-label': labels.MINUTES_TIME_RANGE,
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption',
checked: 'on',
key: 'm',
label: labels.MINUTES,
},
@ -44,10 +47,18 @@ const TimeRangeOptions = [
},
];
export const TimeExpressionSelect: React.FC<Props> = ({ setAlertParams }) => {
const [numUnits, setNumUnits] = useState<number>(15);
export const TimeExpressionSelect: React.FC<Props> = ({
defaultTimerangeCount,
defaultTimerangeUnit,
setAlertParams,
}) => {
const [numUnits, setNumUnits] = useState<number>(defaultTimerangeCount ?? 15);
const [timerangeUnitOptions, setTimerangeUnitOptions] = useState<any[]>(TimeRangeOptions);
const [timerangeUnitOptions, setTimerangeUnitOptions] = useState<any[]>(
TimeRangeOptions.map((opt) =>
opt.key === (defaultTimerangeUnit ?? DEFAULT_TIMERANGE_UNIT) ? { ...opt, checked: 'on' } : opt
)
);
useEffect(() => {
const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm';

View file

@ -0,0 +1,33 @@
/*
* 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 { EuiSpacer, EuiCallOut } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
interface Props {
isOldAlert: boolean;
}
export const OldAlertCallOut: React.FC<Props> = ({ isOldAlert }) => {
if (!isOldAlert) return null;
return (
<>
<EuiSpacer size="m" />
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.oldAlertCallout.title"
defaultMessage="You are editing an older alert, some fields may not auto-populate."
/>
}
iconType="alert"
/>
</>
);
};

View file

@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiLink } from '@elastic/eui';
import { EuiExpression, EuiPopover } from '@elastic/eui';
import { Link } from 'react-router-dom';
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { SETTINGS_ROUTE } from '../../../../common/constants';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
interface SettingsMessageExpressionPopoverProps {
'aria-label': string;
@ -25,9 +25,12 @@ export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressio
value,
id,
}) => {
const kibana = useKibana();
const path = kibana.services?.application?.getUrlForApp('uptime', { path: 'settings' });
const [isOpen, setIsOpen] = useState(false);
return (
<EuiPopover
data-test-subj={`xpack.uptime.alerts.tls.expressionPopover.${id}`}
id={id}
anchorPosition="downLeft"
button={
@ -50,7 +53,7 @@ export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressio
settingsPageLink: (
// this link is wrapped around a span so we can also change the UI state
// and hide the alert flyout before triggering the navigation to the settings page
<Link to={SETTINGS_ROUTE} data-test-subj="xpack.uptime.alerts.tlsFlyout.linkToSettings">
<EuiLink href={path} data-test-subj="xpack.uptime.alerts.tlsFlyout.linkToSettings">
<span
onClick={() => {
setAlertFlyoutVisible(false);
@ -63,7 +66,7 @@ export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressio
>
settings page
</span>
</Link>
</EuiLink>
),
}}
/>

View file

@ -36,13 +36,19 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
interface Props {
'aria-label': string;
autocomplete: DataPublicPluginSetup['autocomplete'];
defaultKuery?: string;
'data-test-subj': string;
shouldUpdateUrl?: boolean;
updateDefaultKuery?: (value: string) => void;
}
export function KueryBar({
'aria-label': ariaLabel,
autocomplete: autocompleteService,
defaultKuery,
'data-test-subj': dataTestSubj,
shouldUpdateUrl,
updateDefaultKuery,
}: Props) {
const { loading, index_pattern: indexPattern } = useIndexPattern();
const { updateSearchText } = useSearchText();
@ -68,8 +74,6 @@ export function KueryBar({
return;
}
updateSearchText(inputValue);
setIsLoadingSuggestions(true);
setState({ ...state, suggestions: [] });
@ -112,7 +116,13 @@ export function KueryBar({
return;
}
updateUrlParams({ search: inputValue.trim() });
if (shouldUpdateUrl !== false) {
updateUrlParams({ search: inputValue.trim() });
}
updateSearchText(inputValue);
if (updateDefaultKuery) {
updateDefaultKuery(inputValue);
}
} catch (e) {
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
}
@ -125,7 +135,7 @@ export function KueryBar({
data-test-subj={dataTestSubj}
disabled={indexPatternMissing}
isLoading={isLoadingSuggestions || loading}
initialValue={kuery}
initialValue={defaultKuery || kuery}
onChange={onChange}
onSubmit={onSubmit}
suggestions={state.suggestions}

View file

@ -6,7 +6,7 @@
import React from 'react';
import { FilterStatusButton, FilterStatusButtonProps } from '../filter_status_button';
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../../lib';
describe('FilterStatusButton', () => {
let props: FilterStatusButtonProps;
@ -26,7 +26,11 @@ describe('FilterStatusButton', () => {
});
it('renders without errors for valid props', () => {
const wrapper = renderWithRouter(<FilterStatusButton {...props} />);
const wrapper = renderWithRouter(
<MountWithReduxProvider>
<FilterStatusButton {...props} />
</MountWithReduxProvider>
);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -5,7 +5,12 @@
*/
import React from 'react';
import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../../lib';
import {
mountWithRouter,
renderWithRouter,
shallowWithRouter,
MountWithReduxProvider,
} from '../../../../lib';
import { createMemoryHistory } from 'history';
import { StatusFilter } from '../status_filter';
import { FilterStatusButton } from '../filter_status_button';
@ -18,7 +23,12 @@ describe('StatusFilterComponent', () => {
initialEntries: [`/?g=%22%22&statusFilter=${status}`],
});
const wrapper = mountWithRouter(<StatusFilter />, history);
const wrapper = mountWithRouter(
<MountWithReduxProvider>
<StatusFilter />
</MountWithReduxProvider>,
history
);
const filterBtns = wrapper.find(FilterStatusButton);
const allBtn = filterBtns.at(0);
@ -34,7 +44,11 @@ describe('StatusFilterComponent', () => {
});
it('renders without errors for valid props', () => {
const wrapper = renderWithRouter(<StatusFilter />);
const wrapper = renderWithRouter(
<MountWithReduxProvider>
<StatusFilter />
</MountWithReduxProvider>
);
expect(wrapper).toMatchSnapshot();
});

View file

@ -137,30 +137,95 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = `
}
}
>
<UseUrlParamsTestComponent
hook={[Function]}
updateParams={
Object {
"pagination": "",
<MountWithReduxProvider>
<Provider
store={
Object {
"dispatch": [MockFunction],
"getState": [MockFunction] {
"calls": Array [
Array [],
Array [],
Array [],
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
],
},
"replaceReducer": [MockFunction],
"subscribe": [MockFunction] {
"calls": Array [
Array [
[Function],
],
Array [
[Function],
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
},
}
}
}
>
<div>
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","pagination":"foo"}
</div>
<button
id="setUrlParams"
onClick={[Function]}
>
Set url params
</button>
<button
id="getUrlParams"
onClick={[Function]}
>
Get url params
</button>
</UseUrlParamsTestComponent>
<UseUrlParamsTestComponent
hook={[Function]}
updateParams={
Object {
"pagination": "",
}
}
>
<div>
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":"","pagination":"foo"}
</div>
<button
id="setUrlParams"
onClick={[Function]}
>
Set url params
</button>
<button
id="getUrlParams"
onClick={[Function]}
>
Get url params
</button>
</UseUrlParamsTestComponent>
</Provider>
</MountWithReduxProvider>
</Router>
`;
@ -301,24 +366,89 @@ exports[`useUrlParams gets the expected values using the context 1`] = `
}
}
>
<UseUrlParamsTestComponent
hook={[Function]}
>
<div>
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":""}
</div>
<button
id="setUrlParams"
onClick={[Function]}
<MountWithReduxProvider>
<Provider
store={
Object {
"dispatch": [MockFunction],
"getState": [MockFunction] {
"calls": Array [
Array [],
Array [],
Array [],
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
Object {
"type": "return",
"value": Object {
"selectedFilters": null,
},
},
],
},
"replaceReducer": [MockFunction],
"subscribe": [MockFunction] {
"calls": Array [
Array [
[Function],
],
Array [
[Function],
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
Object {
"type": "return",
"value": undefined,
},
],
},
}
}
>
Set url params
</button>
<button
id="getUrlParams"
onClick={[Function]}
>
Get url params
</button>
</UseUrlParamsTestComponent>
<UseUrlParamsTestComponent
hook={[Function]}
>
<div>
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":""}
</div>
<button
id="setUrlParams"
onClick={[Function]}
>
Set url params
</button>
<button
id="getUrlParams"
onClick={[Function]}
>
Get url params
</button>
</UseUrlParamsTestComponent>
</Provider>
</MountWithReduxProvider>
</Router>
`;

View file

@ -10,7 +10,7 @@ import { Route } from 'react-router-dom';
import { mountWithRouter } from '../../lib';
import { OVERVIEW_ROUTE } from '../../../common/constants';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { UptimeUrlParams, getSupportedUrlParams } from '../../lib/helper';
import { UptimeUrlParams, getSupportedUrlParams, MountWithReduxProvider } from '../../lib/helper';
import { makeBaseBreadcrumb, useBreadcrumbs } from '../use_breadcrumbs';
describe('useBreadcrumbs', () => {
@ -34,11 +34,13 @@ describe('useBreadcrumbs', () => {
};
mountWithRouter(
<KibanaContextProvider services={{ ...core }}>
<Route path={OVERVIEW_ROUTE}>
<Component />
</Route>
</KibanaContextProvider>
<MountWithReduxProvider>
<KibanaContextProvider services={{ ...core }}>
<Route path={OVERVIEW_ROUTE}>
<Component />
</Route>
</KibanaContextProvider>
</MountWithReduxProvider>
);
const urlParams: UptimeUrlParams = getSupportedUrlParams({});

View file

@ -8,7 +8,7 @@ import DateMath from '@elastic/datemath';
import React, { useState, Fragment } from 'react';
import { useUrlParams, UptimeUrlParamsHook } from '../use_url_params';
import { UptimeRefreshContext } from '../../contexts';
import { mountWithRouter } from '../../lib';
import { mountWithRouter, MountWithReduxProvider } from '../../lib';
import { createMemoryHistory } from 'history';
interface MockUrlParamsComponentProps {
@ -52,9 +52,11 @@ describe('useUrlParams', () => {
jest.spyOn(history, 'push');
const component = mountWithRouter(
<UptimeRefreshContext.Provider value={{ lastRefresh: 123, refreshApp: jest.fn() }}>
<UseUrlParamsTestComponent hook={useUrlParams} />
</UptimeRefreshContext.Provider>,
<MountWithReduxProvider>
<UptimeRefreshContext.Provider value={{ lastRefresh: 123, refreshApp: jest.fn() }}>
<UseUrlParamsTestComponent hook={useUrlParams} />
</UptimeRefreshContext.Provider>
</MountWithReduxProvider>,
history
);
@ -68,14 +70,16 @@ describe('useUrlParams', () => {
it('gets the expected values using the context', () => {
const component = mountWithRouter(
<UptimeRefreshContext.Provider
value={{
lastRefresh: 123,
refreshApp: jest.fn(),
}}
>
<UseUrlParamsTestComponent hook={useUrlParams} />
</UptimeRefreshContext.Provider>
<MountWithReduxProvider>
<UptimeRefreshContext.Provider
value={{
lastRefresh: 123,
refreshApp: jest.fn(),
}}
>
<UseUrlParamsTestComponent hook={useUrlParams} />
</UptimeRefreshContext.Provider>
</MountWithReduxProvider>
);
const getUrlParamsButton = component.find('#getUrlParams');
@ -92,14 +96,16 @@ describe('useUrlParams', () => {
jest.spyOn(history, 'push');
const component = mountWithRouter(
<UptimeRefreshContext.Provider
value={{
lastRefresh: 123,
refreshApp: jest.fn(),
}}
>
<UseUrlParamsTestComponent hook={useUrlParams} updateParams={{ pagination: '' }} />
</UptimeRefreshContext.Provider>,
<MountWithReduxProvider>
<UptimeRefreshContext.Provider
value={{
lastRefresh: 123,
refreshApp: jest.fn(),
}}
>
<UseUrlParamsTestComponent hook={useUrlParams} updateParams={{ pagination: '' }} />
</UptimeRefreshContext.Provider>
</MountWithReduxProvider>,
history
);

View file

@ -20,14 +20,18 @@ interface SelectedFilters {
selectedFilters: Map<string, string[]>;
}
export const useFilterUpdate = (fieldName?: string, values?: string[]): SelectedFilters => {
export const useFilterUpdate = (
fieldName?: string,
values?: string[],
shouldUpdateUrl: boolean = true
): SelectedFilters => {
const [getUrlParams, updateUrl] = useUrlParams();
const { filters: currentFilters } = getUrlParams();
// update filters in the URL from filter group
const onFilterUpdate = (filtersKuery: string) => {
if (currentFilters !== filtersKuery) {
if (currentFilters !== filtersKuery && shouldUpdateUrl) {
updateUrl({ filters: filtersKuery, pagination: '' });
}
};

View file

@ -4,9 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect } from 'react';
import { parse, stringify } from 'query-string';
import { useLocation, useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper';
import { selectedFiltersSelector } from '../state/selectors';
import { setSelectedFilters } from '../state/actions/selected_filters';
export type GetUrlParams = () => UptimeUrlParams;
export type UpdateUrlParams = (updatedParams: {
@ -27,9 +31,35 @@ export const useGetUrlParams: GetUrlParams = () => {
return getSupportedUrlParams(params);
};
const getMapFromFilters = (value: any): Map<string, any> | undefined => {
try {
return new Map(JSON.parse(value));
} catch {
return undefined;
}
};
const mapMapToObject = (map: Map<string, any>) => ({
locations: map.get('observer.geo.name') ?? [],
ports: map.get('url.port') ?? [],
schemes: map.get('monitor.type') ?? [],
tags: map.get('tags') ?? [],
});
export const useUrlParams: UptimeUrlParamsHook = () => {
const location = useLocation();
const history = useHistory();
const dispatch = useDispatch();
const selectedFilters = useSelector(selectedFiltersSelector);
const { filters } = useGetUrlParams();
useEffect(() => {
if (selectedFilters === null) {
const filterMap = getMapFromFilters(filters);
if (filterMap) {
dispatch(setSelectedFilters(mapMapToObject(filterMap)));
}
}
}, [dispatch, filters, selectedFilters]);
const updateUrlParams: UpdateUrlParams = (updatedParams) => {
if (!history || !location) return;
@ -57,6 +87,12 @@ export const useUrlParams: UptimeUrlParamsHook = () => {
{ sort: false }
),
});
const filterMap = getMapFromFilters(mergedParams.filters);
if (!filterMap) {
dispatch(setSelectedFilters(null));
} else {
dispatch(setSelectedFilters(mapMapToObject(filterMap)));
}
};
return [useGetUrlParams, updateUrlParams];

View file

@ -9,7 +9,6 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { get } from 'lodash';
import { i18n as i18nFormatter } from '@kbn/i18n';
import { alertTypeInitializers } from '../../alert_types';
import { UptimeApp, UptimeAppProps } from '../../../uptime_app';
import { getIntegratedAppAvailability } from './capabilities_adapter';
import {
@ -34,18 +33,6 @@ export const getKibanaFrameworkAdapter = (
i18n,
} = core;
const {
data: { autocomplete },
triggers_actions_ui,
} = plugins;
alertTypeInitializers.forEach((init) => {
const alertInitializer = init({ autocomplete });
if (!triggers_actions_ui.alertTypeRegistry.has(alertInitializer.id)) {
triggers_actions_ui.alertTypeRegistry.register(init({ autocomplete }));
}
});
const { apm, infrastructure, logs } = getIntegratedAppAvailability(
capabilities,
INTEGRATED_SOLUTIONS

View file

@ -33,6 +33,24 @@ describe('monitor status alert type', () => {
`);
});
it('accepts original alert params', () => {
expect(
validate({
locations: ['fairbanks'],
numTimes: 3,
timerange: {
from: 'now-15m',
to: 'now',
},
filters: '{foo: "bar"}',
})
).toMatchInlineSnapshot(`
Object {
"errors": Object {},
}
`);
});
describe('timerange', () => {
it('has invalid timerangeCount value', () => {
expect(validate({ ...params, timerangeCount: 0 })).toMatchInlineSnapshot(`
@ -96,7 +114,22 @@ describe('monitor status alert type', () => {
});
describe('initMonitorStatusAlertType', () => {
expect(initMonitorStatusAlertType({ autocomplete: {} })).toMatchInlineSnapshot(`
expect(
initMonitorStatusAlertType({
store: {
dispatch: jest.fn(),
getState: jest.fn(),
replaceReducer: jest.fn(),
subscribe: jest.fn(),
[Symbol.observable]: jest.fn(),
},
// @ts-ignore we don't need to test this functionality here because
// it's not used by the code this file tests
core: {},
// @ts-ignore
plugins: {},
})
).toMatchInlineSnapshot(`
Object {
"alertParamsExpression": [Function],
"defaultActionMessage": "{{context.message}}
@ -104,8 +137,20 @@ describe('monitor status alert type', () => {
{{context.downMonitorsWithGeo}}",
"iconClass": "uptimeApp",
"id": "xpack.uptime.alerts.monitorStatus",
"name": <MonitorStatusTitle />,
"requiresAppContext": true,
"name": <Provider
store={
Object {
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(observable): [Function],
}
}
>
<MonitorStatusTitle />
</Provider>,
"requiresAppContext": false,
"validate": [Function],
}
`);

View file

@ -4,11 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { CoreStart } from 'kibana/public';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { initMonitorStatusAlertType } from './monitor_status';
import { initTlsAlertType } from './tls';
import { ClientPluginsStart } from '../../apps/plugin';
export type AlertTypeInitializer = (dependenies: { autocomplete: any }) => AlertTypeModel;
export type AlertTypeInitializer = (dependenies: {
core: CoreStart;
plugins: ClientPluginsStart;
}) => AlertTypeModel;
export const alertTypeInitializers: AlertTypeInitializer[] = [
initMonitorStatusAlertType,

View file

@ -4,24 +4,33 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Provider as ReduxProvider } from 'react-redux';
import React from 'react';
import { isRight } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { AlertTypeInitializer } from '.';
import { AtomicStatusCheckParamsType } from '../../../common/runtime_types';
import { AtomicStatusCheckParamsType, StatusCheckParamsType } from '../../../common/runtime_types';
import { MonitorStatusTitle } from './monitor_status_title';
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
import { MonitorStatusTranslations } from './translations';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { store } from '../../state';
export const validate = (alertParams: unknown) => {
const errors: Record<string, any> = {};
const decoded = AtomicStatusCheckParamsType.decode(alertParams);
const oldDecoded = StatusCheckParamsType.decode(alertParams);
if (!isRight(decoded)) {
errors.typeCheckFailure = 'Provided parameters do not conform to the expected type.';
errors.typeCheckParsingMessage = PathReporter.report(decoded);
} else {
if (!isRight(decoded) && !isRight(oldDecoded)) {
return {
errors: {
typeCheckFailure: 'Provided parameters do not conform to the expected type.',
typeCheckParsingMessage: PathReporter.report(decoded),
},
};
}
if (isRight(decoded)) {
const { numTimes, timerangeCount } = decoded.right;
if (numTimes < 1) {
errors.invalidNumTimes = 'Number of alert check down times must be an integer greater than 0';
@ -44,15 +53,26 @@ const AlertMonitorStatus = React.lazy(() =>
);
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
autocomplete,
core,
plugins,
}): AlertTypeModel => ({
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
name: <MonitorStatusTitle />,
iconClass: 'uptimeApp',
alertParamsExpression: (params: any) => (
<AlertMonitorStatus {...params} autocomplete={autocomplete} />
name: (
<ReduxProvider store={store}>
<MonitorStatusTitle />
</ReduxProvider>
),
iconClass: 'uptimeApp',
alertParamsExpression: (params: any) => {
return (
<ReduxProvider store={store}>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<AlertMonitorStatus {...params} autocomplete={plugins.data.autocomplete} />
</KibanaContextProvider>
</ReduxProvider>
);
},
validate,
defaultActionMessage,
requiresAppContext: true,
requiresAppContext: false,
});

View file

@ -5,30 +5,13 @@
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import { snapshotDataSelector } from '../../state/selectors';
export const MonitorStatusTitle = () => {
const { count, loading } = useSelector(snapshotDataSelector);
return (
<EuiFlexGroup>
<EuiFlexItem>
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.title.label"
defaultMessage="Uptime monitor status"
/>{' '}
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
{!loading ? (
<EuiText size="s" color="subdued">
{count.total} monitors
</EuiText>
) : (
<EuiLoadingSpinner size="m" />
)}
</EuiFlexItem>
</EuiFlexGroup>
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.title.label"
defaultMessage="Uptime monitor status"
/>
);
};

View file

@ -5,21 +5,30 @@
*/
import React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
import { TlsTranslations } from './translations';
import { AlertTypeInitializer } from '.';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { store } from '../../state';
const { name, defaultActionMessage } = TlsTranslations;
export const initTlsAlertType: AlertTypeInitializer = (): AlertTypeModel => ({
const TlsAlertExpression = React.lazy(() =>
import('../../components/overview/alerts/alerts_containers/alert_tls')
);
export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({
id: CLIENT_ALERT_TYPES.TLS,
iconClass: 'uptimeApp',
alertParamsExpression: React.lazy(() =>
import('../../components/overview/alerts/alerts_containers/alert_tls')
alertParamsExpression: (_params: any) => (
<ReduxProvider store={store}>
<KibanaContextProvider services={{ ...core, ...plugins }}>
<TlsAlertExpression />
</KibanaContextProvider>
</ReduxProvider>
),
name,
validate: () => ({ errors: {} }),
defaultActionMessage,
requiresAppContext: true,
requiresAppContext: false,
});

View file

@ -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 React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
export const MountWithReduxProvider: React.FC = ({ children }) => (
<ReduxProvider
store={{
dispatch: jest.fn(),
getState: jest.fn().mockReturnValue({ selectedFilters: null }),
subscribe: jest.fn(),
replaceReducer: jest.fn(),
}}
>
{children}
</ReduxProvider>
);

View file

@ -9,3 +9,4 @@ export * from './observability_integration';
export { getChartDateLabel } from './charts';
export { seriesHasDownValues } from './series_has_down_values';
export { UptimeUrlParams, getSupportedUrlParams } from './url_params';
export { MountWithReduxProvider } from './helper_with_redux';

View file

@ -4,4 +4,5 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { MountWithReduxProvider } from './helper';
export { renderWithRouter, shallowWithRouter, mountWithRouter } from './helper/helper_with_router';

View file

@ -6,47 +6,33 @@
import React from 'react';
import { PageHeader } from '../page_header';
import { renderWithRouter } from '../../lib';
import { Provider } from 'react-redux';
import { renderWithRouter, MountWithReduxProvider } from '../../lib';
describe('PageHeader', () => {
it('shallow renders with the date picker', () => {
const component = renderWithRouter(
<MockReduxProvider>
<MountWithReduxProvider>
<PageHeader headingText={'TestingHeading'} datePicker={true} />
</MockReduxProvider>
</MountWithReduxProvider>
);
expect(component).toMatchSnapshot('page_header_with_date_picker');
});
it('shallow renders without the date picker', () => {
const component = renderWithRouter(
<MockReduxProvider>
<MountWithReduxProvider>
<PageHeader headingText={'TestingHeading'} datePicker={false} />
</MockReduxProvider>
</MountWithReduxProvider>
);
expect(component).toMatchSnapshot('page_header_no_date_picker');
});
it('shallow renders extra links', () => {
const component = renderWithRouter(
<MockReduxProvider>
<MountWithReduxProvider>
<PageHeader headingText={'TestingHeading'} extraLinks={true} datePicker={true} />
</MockReduxProvider>
</MountWithReduxProvider>
);
expect(component).toMatchSnapshot('page_header_with_extra_links');
});
});
const MockReduxProvider = ({ children }: { children: React.ReactElement }) => (
<Provider
store={{
dispatch: jest.fn(),
getState: jest.fn(),
subscribe: jest.fn(),
replaceReducer: jest.fn(),
}}
>
{children}
</Provider>
);

View file

@ -9,6 +9,7 @@ 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 const SET_OVERVIEW_FILTERS = 'SET_OVERVIEW_FILTERS';
export interface GetOverviewFiltersPayload {
dateRangeStart: string;
@ -36,10 +37,16 @@ interface GetOverviewFiltersFailAction {
payload: Error;
}
interface SetOverviewFiltersAction {
type: typeof SET_OVERVIEW_FILTERS;
payload: OverviewFilters;
}
export type OverviewFiltersAction =
| GetOverviewFiltersFetchAction
| GetOverviewFiltersSuccessAction
| GetOverviewFiltersFailAction;
| GetOverviewFiltersFailAction
| SetOverviewFiltersAction;
export const fetchOverviewFilters = (
payload: GetOverviewFiltersPayload
@ -59,3 +66,8 @@ export const fetchOverviewFiltersSuccess = (
type: FETCH_OVERVIEW_FILTERS_SUCCESS,
payload: filters,
});
export const setOverviewFilters = (filters: OverviewFilters): SetOverviewFiltersAction => ({
type: SET_OVERVIEW_FILTERS,
payload: filters,
});

View file

@ -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 { createAction } from 'redux-actions';
export interface SelectedFilters {
locations: string[];
ports: number[];
schemes: string[];
tags: string[];
}
export type SelectedFiltersPayload = SelectedFilters;
export const getSelectedFilters = createAction<void>('GET SELECTED FILTERS');
export const setSelectedFilters = createAction<SelectedFiltersPayload | null>(
'SET_SELECTED_FILTERS'
);

View file

@ -9,12 +9,12 @@ import createSagaMiddleware from 'redux-saga';
import { rootEffect } from './effects';
import { rootReducer } from './reducers';
export type AppState = ReturnType<typeof rootReducer>;
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const sagaMW = createSagaMiddleware();
export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMW)));
export type AppState = ReturnType<typeof rootReducer>;
sagaMW.run(rootEffect);

View file

@ -4,7 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { setBasePath, toggleIntegrationsPopover, setAlertFlyoutVisible } from '../../actions';
import {
setBasePath,
toggleIntegrationsPopover,
setAlertFlyoutVisible,
setSearchTextAction,
} from '../../actions';
import { uiReducer } from '../ui';
import { Action } from 'redux-actions';
@ -67,4 +72,28 @@ describe('ui reducer', () => {
}
`);
});
it('sets the search text', () => {
const action = setSearchTextAction('lorem ipsum') as Action<never>;
expect(
uiReducer(
{
alertFlyoutVisible: false,
basePath: '',
esKuery: '',
integrationsPopoverOpen: null,
searchText: '',
},
action
)
).toMatchInlineSnapshot(`
Object {
"alertFlyoutVisible": false,
"basePath": "",
"esKuery": "",
"integrationsPopoverOpen": null,
"searchText": "lorem ipsum",
}
`);
});
});

View file

@ -19,6 +19,7 @@ import { monitorDurationReducer } from './monitor_duration';
import { indexStatusReducer } from './index_status';
import { mlJobsReducer } from './ml_anomaly';
import { certificatesReducer } from '../certificates/certificates';
import { selectedFiltersReducer } from './selected_filters';
export const rootReducer = combineReducers({
monitor: monitorReducer,
@ -35,4 +36,5 @@ export const rootReducer = combineReducers({
monitorDuration: monitorDurationReducer,
indexStatus: indexStatusReducer,
certificates: certificatesReducer,
selectedFilters: selectedFiltersReducer,
});

View file

@ -10,6 +10,7 @@ import {
FETCH_OVERVIEW_FILTERS_FAIL,
FETCH_OVERVIEW_FILTERS_SUCCESS,
OverviewFiltersAction,
SET_OVERVIEW_FILTERS,
} from '../actions';
export interface OverviewFiltersState {
@ -51,6 +52,11 @@ export function overviewFiltersReducer(
errors: [...state.errors, action.payload],
loading: false,
};
case SET_OVERVIEW_FILTERS:
return {
...state,
filters: action.payload,
};
default:
return state;
}

View file

@ -0,0 +1,32 @@
/*
* 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 { Action } from 'redux-actions';
import {
getSelectedFilters,
setSelectedFilters,
SelectedFilters,
} from '../actions/selected_filters';
const initialState: SelectedFilters | null = null;
export function selectedFiltersReducer(
state = initialState,
action: Action<any>
): SelectedFilters | null {
switch (action.type) {
case String(getSelectedFilters):
return state;
case String(setSelectedFilters):
if (state === null) return { ...action.payload };
return {
...(state || {}),
...action.payload,
};
default:
return state;
}
}

View file

@ -107,6 +107,7 @@ describe('state selectors', () => {
loading: false,
},
},
selectedFilters: null,
};
it('selects base path from state', () => {

View file

@ -86,3 +86,5 @@ export const overviewFiltersSelector = ({ overviewFilters }: AppState) => overvi
export const esKuerySelector = ({ ui: { esKuery } }: AppState) => esKuery;
export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchText;
export const selectedFiltersSelector = ({ selectedFilters }: AppState) => selectedFilters;

View file

@ -20,14 +20,13 @@ import {
UptimeStartupPluginsContextProvider,
} from './contexts';
import { CommonlyUsedRange } from './components/common/uptime_date_picker';
import { store } from './state';
import { setBasePath } from './state/actions';
import { PageRouter } from './routes';
import {
UptimeAlertsContextProvider,
UptimeAlertsFlyoutWrapper,
} from './components/overview/alerts';
import { kibanaService } from './state/kibana_service';
import { store } from './state';
export interface UptimeAppColors {
danger: string;
@ -87,8 +86,6 @@ const Application = (props: UptimeAppProps) => {
);
}, [canSave, renderGlobalHelpControls, setBadge]);
kibanaService.core = core;
store.dispatch(setBasePath(basePath));
return (