[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:
parent
891342a76f
commit
858523eac6
|
@ -3,6 +3,7 @@
|
||||||
* or more contributor license agreements. Licensed under the Elastic License;
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CoreSetup,
|
CoreSetup,
|
||||||
CoreStart,
|
CoreStart,
|
||||||
|
@ -16,8 +17,16 @@ import { PLUGIN } from '../../common/constants';
|
||||||
import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public';
|
import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public';
|
||||||
import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public';
|
import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public';
|
||||||
import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public';
|
import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public';
|
||||||
import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public';
|
import {
|
||||||
import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public';
|
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 {
|
export interface ClientPluginsSetup {
|
||||||
data: DataPublicPluginSetup;
|
data: DataPublicPluginSetup;
|
||||||
|
@ -27,6 +36,8 @@ export interface ClientPluginsSetup {
|
||||||
|
|
||||||
export interface ClientPluginsStart {
|
export interface ClientPluginsStart {
|
||||||
embeddable: EmbeddableStart;
|
embeddable: EmbeddableStart;
|
||||||
|
data: DataPublicPluginStart;
|
||||||
|
triggers_actions_ui: TriggersAndActionsUIPublicPluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ClientSetup = void;
|
export type ClientSetup = void;
|
||||||
|
@ -66,6 +77,7 @@ export class UptimePlugin
|
||||||
);
|
);
|
||||||
|
|
||||||
const { element } = params;
|
const { element } = params;
|
||||||
|
|
||||||
const libs: UMFrontendLibs = {
|
const libs: UMFrontendLibs = {
|
||||||
framework: getKibanaFrameworkAdapter(coreStart, plugins, corePlugins),
|
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 {}
|
public stop(): void {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { UptimeDatePicker } from '../uptime_date_picker';
|
import { UptimeDatePicker } from '../uptime_date_picker';
|
||||||
import { renderWithRouter, shallowWithRouter } from '../../../lib';
|
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../lib';
|
||||||
|
|
||||||
describe('UptimeDatePicker component', () => {
|
describe('UptimeDatePicker component', () => {
|
||||||
it('validates props with shallow render', () => {
|
it('validates props with shallow render', () => {
|
||||||
|
@ -15,7 +15,11 @@ describe('UptimeDatePicker component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders properly with mock data', () => {
|
it('renders properly with mock data', () => {
|
||||||
const component = renderWithRouter(<UptimeDatePicker />);
|
const component = renderWithRouter(
|
||||||
|
<MountWithReduxProvider>
|
||||||
|
<UptimeDatePicker />
|
||||||
|
</MountWithReduxProvider>
|
||||||
|
);
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series';
|
import { MonitorBarSeries, MonitorBarSeriesProps } from '../monitor_bar_series';
|
||||||
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../../lib';
|
||||||
import { HistogramPoint } from '../../../../../common/runtime_types';
|
import { HistogramPoint } from '../../../../../common/runtime_types';
|
||||||
|
|
||||||
describe('MonitorBarSeries component', () => {
|
describe('MonitorBarSeries component', () => {
|
||||||
|
@ -197,7 +197,11 @@ describe('MonitorBarSeries component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders if the data series is present', () => {
|
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();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PingHistogramComponent, PingHistogramComponentProps } from '../ping_histogram';
|
import { PingHistogramComponent, PingHistogramComponentProps } from '../ping_histogram';
|
||||||
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../../lib';
|
||||||
|
|
||||||
describe('PingHistogram component', () => {
|
describe('PingHistogram component', () => {
|
||||||
const props: PingHistogramComponentProps = {
|
const props: PingHistogramComponentProps = {
|
||||||
|
@ -49,7 +49,12 @@ describe('PingHistogram component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the component without errors', () => {
|
it('renders the component without errors', () => {
|
||||||
const component = renderWithRouter(<PingHistogramComponent {...props} />);
|
const component = renderWithRouter(
|
||||||
|
<MountWithReduxProvider>
|
||||||
|
<PingHistogramComponent {...props} />
|
||||||
|
</MountWithReduxProvider>
|
||||||
|
);
|
||||||
|
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,141 +5,120 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||||
import { AlertFieldNumber, handleAlertFieldNumberChange } from '../alert_field_number';
|
import { AlertMonitorStatusComponent, AlertMonitorStatusProps } from '../alert_monitor_status';
|
||||||
|
|
||||||
describe('alert monitor status component', () => {
|
describe('alert monitor status component', () => {
|
||||||
describe('handleAlertFieldNumberChange', () => {
|
describe('AlertMonitorStatus', () => {
|
||||||
let mockSetIsInvalid: jest.Mock;
|
const defaultProps: AlertMonitorStatusProps = {
|
||||||
let mockSetFieldValue: jest.Mock;
|
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(() => {
|
it('passes default props to children', () => {
|
||||||
mockSetIsInvalid = jest.fn();
|
const component = shallowWithIntl(<AlertMonitorStatusComponent {...defaultProps} />);
|
||||||
mockSetFieldValue = jest.fn();
|
expect(component).toMatchInlineSnapshot(`
|
||||||
});
|
<Fragment>
|
||||||
|
<OldAlertCallOut
|
||||||
afterEach(() => {
|
isOldAlert={true}
|
||||||
jest.clearAllMocks();
|
/>
|
||||||
});
|
<EuiSpacer
|
||||||
|
size="m"
|
||||||
it('sets a valid number', () => {
|
/>
|
||||||
handleAlertFieldNumberChange(
|
<KueryBar
|
||||||
// @ts-ignore no need to implement this entire type here
|
aria-label="Input that allows filtering criteria for the monitor status alert"
|
||||||
{ target: { value: '23' } },
|
autocomplete={
|
||||||
false,
|
Object {
|
||||||
mockSetIsInvalid,
|
"addQuerySuggestionProvider": [MockFunction],
|
||||||
mockSetFieldValue
|
"getQuerySuggestions": [MockFunction],
|
||||||
);
|
}
|
||||||
expect(mockSetIsInvalid).not.toHaveBeenCalled();
|
}
|
||||||
expect(mockSetFieldValue).toHaveBeenCalledTimes(1);
|
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
|
||||||
expect(mockSetFieldValue.mock.calls).toMatchInlineSnapshot(`
|
defaultKuery="monitor.id: foo"
|
||||||
Array [
|
shouldUpdateUrl={false}
|
||||||
Array [
|
updateDefaultKuery={[Function]}
|
||||||
23,
|
/>
|
||||||
],
|
<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([]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,20 +6,18 @@
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||||
import { useFilterUpdate } from '../../../hooks/use_filter_update';
|
|
||||||
import * as labels from './translations';
|
import * as labels from './translations';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
newFilters: string[];
|
newFilters: string[];
|
||||||
onNewFilter: (val: string) => void;
|
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 [isPopoverOpen, setPopover] = useState(false);
|
||||||
|
|
||||||
const { selectedFilters } = useFilterUpdate();
|
const getSelectedItems = (fieldName: string) => alertFilters?.[fieldName] ?? [];
|
||||||
|
|
||||||
const getSelectedItems = (fieldName: string) => selectedFilters.get(fieldName) || [];
|
|
||||||
|
|
||||||
const onButtonClick = () => {
|
const onButtonClick = () => {
|
||||||
setPopover(!isPopoverOpen);
|
setPopover(!isPopoverOpen);
|
||||||
|
|
|
@ -5,25 +5,31 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
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 { DataPublicPluginSetup } from 'src/plugins/data/public';
|
||||||
import * as labels from './translations';
|
import * as labels from './translations';
|
||||||
import {
|
import {
|
||||||
DownNoExpressionSelect,
|
DownNoExpressionSelect,
|
||||||
TimeExpressionSelect,
|
TimeExpressionSelect,
|
||||||
FiltersExpressionsSelect,
|
FiltersExpressionSelectContainer,
|
||||||
} from './monitor_expressions';
|
} from './monitor_expressions';
|
||||||
|
|
||||||
import { AddFilterButton } from './add_filter_btn';
|
import { AddFilterButton } from './add_filter_btn';
|
||||||
|
import { OldAlertCallOut } from './old_alert_call_out';
|
||||||
import { KueryBar } from '..';
|
import { KueryBar } from '..';
|
||||||
|
|
||||||
interface AlertMonitorStatusProps {
|
export interface AlertMonitorStatusProps {
|
||||||
|
alertParams: { [key: string]: any };
|
||||||
autocomplete: DataPublicPluginSetup['autocomplete'];
|
autocomplete: DataPublicPluginSetup['autocomplete'];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
filters: string;
|
hasFilters: boolean;
|
||||||
|
isOldAlert: boolean;
|
||||||
locations: string[];
|
locations: string[];
|
||||||
|
snapshotCount: number;
|
||||||
|
snapshotLoading: boolean;
|
||||||
numTimes: number;
|
numTimes: number;
|
||||||
setAlertParams: (key: string, value: any) => void;
|
setAlertParams: (key: string, value: any) => void;
|
||||||
|
shouldUpdateUrl: boolean;
|
||||||
timerange: {
|
timerange: {
|
||||||
from: string;
|
from: string;
|
||||||
to: string;
|
to: string;
|
||||||
|
@ -31,42 +37,70 @@ interface AlertMonitorStatusProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (props) => {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<OldAlertCallOut isOldAlert={isOldAlert} />
|
||||||
|
|
||||||
<EuiSpacer size="m" />
|
<EuiSpacer size="m" />
|
||||||
|
|
||||||
<KueryBar
|
<KueryBar
|
||||||
aria-label={labels.ALERT_KUERY_BAR_ARIA}
|
aria-label={labels.ALERT_KUERY_BAR_ARIA}
|
||||||
autocomplete={props.autocomplete}
|
autocomplete={props.autocomplete}
|
||||||
|
defaultKuery={alertParams.search}
|
||||||
|
shouldUpdateUrl={shouldUpdateUrl}
|
||||||
|
updateDefaultKuery={(value: string) => setAlertParams('search', value)}
|
||||||
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
|
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EuiSpacer size="s" />
|
<EuiSpacer size="s" />
|
||||||
|
|
||||||
<DownNoExpressionSelect filters={filters} setAlertParams={setAlertParams} />
|
<DownNoExpressionSelect
|
||||||
|
defaultNumTimes={alertParams.numTimes}
|
||||||
<EuiSpacer size="xs" />
|
hasFilters={hasFilters}
|
||||||
|
|
||||||
<TimeExpressionSelect setAlertParams={setAlertParams} />
|
|
||||||
|
|
||||||
<EuiSpacer size="xs" />
|
|
||||||
|
|
||||||
<FiltersExpressionsSelect
|
|
||||||
setAlertParams={setAlertParams}
|
setAlertParams={setAlertParams}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EuiSpacer size="xs" />
|
||||||
|
|
||||||
|
<TimeExpressionSelect
|
||||||
|
defaultTimerangeUnit={alertParams.timerangeUnit}
|
||||||
|
defaultTimerangeCount={alertParams.timerangeCount}
|
||||||
|
setAlertParams={setAlertParams}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EuiSpacer size="xs" />
|
||||||
|
|
||||||
|
<FiltersExpressionSelectContainer
|
||||||
|
alertParams={alertParams}
|
||||||
newFilters={newFilters}
|
newFilters={newFilters}
|
||||||
onRemoveFilter={(removeFiler) => {
|
onRemoveFilter={(removeFilter: string) => {
|
||||||
if (newFilters.includes(removeFiler)) {
|
if (newFilters.includes(removeFilter)) {
|
||||||
setNewFilters(newFilters.filter((item) => item !== removeFiler));
|
setNewFilters(newFilters.filter((item) => item !== removeFilter));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
setAlertParams={setAlertParams}
|
||||||
|
shouldUpdateUrl={shouldUpdateUrl}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EuiSpacer size="xs" />
|
<EuiSpacer size="xs" />
|
||||||
|
|
||||||
<AddFilterButton
|
<AddFilterButton
|
||||||
|
alertFilters={alertParams.filters}
|
||||||
newFilters={newFilters}
|
newFilters={newFilters}
|
||||||
onNewFilter={(newFilter) => {
|
onNewFilter={(newFilter) => {
|
||||||
setNewFilters([...newFilters, newFilter]);
|
setNewFilters([...newFilters, newFilter]);
|
||||||
|
@ -74,6 +108,20 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (p
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EuiSpacer size="m" />
|
<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" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,13 +4,31 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { DataPublicPluginSetup } from 'src/plugins/data/public';
|
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 { 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 {
|
interface Props {
|
||||||
|
alertParams: { [key: string]: any };
|
||||||
autocomplete: DataPublicPluginSetup['autocomplete'];
|
autocomplete: DataPublicPluginSetup['autocomplete'];
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
numTimes: number;
|
numTimes: number;
|
||||||
|
@ -27,22 +45,87 @@ export const AlertMonitorStatus: React.FC<Props> = ({
|
||||||
numTimes,
|
numTimes,
|
||||||
setAlertParams,
|
setAlertParams,
|
||||||
timerange,
|
timerange,
|
||||||
|
alertParams,
|
||||||
}) => {
|
}) => {
|
||||||
const { filters, locations } = useSelector(selectMonitorStatusAlert);
|
const dispatch = useDispatch();
|
||||||
const searchText = useSelector(searchTextSelector);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAlertParams('search', searchText);
|
dispatch(
|
||||||
}, [setAlertParams, searchText]);
|
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 (
|
return (
|
||||||
<AlertMonitorStatusComponent
|
<AlertMonitorStatusComponent
|
||||||
|
alertParams={alertParams}
|
||||||
autocomplete={autocomplete}
|
autocomplete={autocomplete}
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
filters={filters}
|
hasFilters={!!overviewFilters?.filters}
|
||||||
|
isOldAlert={isOldAlert}
|
||||||
locations={locations}
|
locations={locations}
|
||||||
numTimes={numTimes}
|
numTimes={numTimes}
|
||||||
setAlertParams={setAlertParams}
|
setAlertParams={setAlertParams}
|
||||||
|
shouldUpdateUrl={shouldUpdateUrl}
|
||||||
|
snapshotCount={count.total}
|
||||||
|
snapshotLoading={loading}
|
||||||
timerange={timerange}
|
timerange={timerange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,20 +9,16 @@ import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||||
import { DownNoExpressionSelect } from '../down_number_select';
|
import { DownNoExpressionSelect } from '../down_number_select';
|
||||||
|
|
||||||
describe('DownNoExpressionSelect component', () => {
|
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 () {
|
it('should shallow renders against props', function () {
|
||||||
const component = shallowWithIntl(
|
const component = shallowWithIntl(
|
||||||
<DownNoExpressionSelect filters={filters} setAlertParams={jest.fn()} />
|
<DownNoExpressionSelect hasFilters={true} setAlertParams={jest.fn()} />
|
||||||
);
|
);
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should renders against props', function () {
|
it('should renders against props', function () {
|
||||||
const component = renderWithIntl(
|
const component = renderWithIntl(
|
||||||
<DownNoExpressionSelect filters={filters} setAlertParams={jest.fn()} />
|
<DownNoExpressionSelect hasFilters={true} setAlertParams={jest.fn()} />
|
||||||
);
|
);
|
||||||
expect(component).toMatchSnapshot();
|
expect(component).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,12 +10,17 @@ import * as labels from '../translations';
|
||||||
import { AlertFieldNumber } from '../alert_field_number';
|
import { AlertFieldNumber } from '../alert_field_number';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
defaultNumTimes?: number;
|
||||||
|
hasFilters: boolean;
|
||||||
setAlertParams: (key: string, value: any) => void;
|
setAlertParams: (key: string, value: any) => void;
|
||||||
filters: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DownNoExpressionSelect: React.FC<Props> = ({ filters, setAlertParams }) => {
|
export const DownNoExpressionSelect: React.FC<Props> = ({
|
||||||
const [numTimes, setNumTimes] = useState<number>(5);
|
defaultNumTimes,
|
||||||
|
hasFilters,
|
||||||
|
setAlertParams,
|
||||||
|
}) => {
|
||||||
|
const [numTimes, setNumTimes] = useState<number>(defaultNumTimes ?? 5);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAlertParams('numTimes', numTimes);
|
setAlertParams('numTimes', numTimes);
|
||||||
|
@ -34,7 +39,7 @@ export const DownNoExpressionSelect: React.FC<Props> = ({ filters, setAlertParam
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression"
|
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"
|
id="ping-count"
|
||||||
value={`${numTimes} times`}
|
value={`${numTimes} times`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,57 +4,55 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { EuiButtonIcon, EuiExpression, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
import { EuiButtonIcon, EuiExpression, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||||
import { FilterPopover } from '../../filter_group/filter_popover';
|
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 { filterLabels } from '../../filter_group/translations';
|
||||||
import { alertFilterLabels } from './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 {
|
type FilterFieldUpdate = (updateTarget: { fieldName: string; values: string[] }) => void;
|
||||||
newFilters: string[];
|
|
||||||
onRemoveFilter: (val: string) => void;
|
interface OwnProps {
|
||||||
setAlertParams: (key: string, value: any) => void;
|
setUpdatedFieldValues: FilterFieldUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Props = FilterExpressionsSelectProps & Pick<OverviewFiltersState, 'filters'> & OwnProps;
|
||||||
|
|
||||||
export const FiltersExpressionsSelect: React.FC<Props> = ({
|
export const FiltersExpressionsSelect: React.FC<Props> = ({
|
||||||
setAlertParams,
|
alertParams,
|
||||||
|
filters: overviewFilters,
|
||||||
newFilters,
|
newFilters,
|
||||||
onRemoveFilter,
|
onRemoveFilter,
|
||||||
|
setAlertParams,
|
||||||
|
setUpdatedFieldValues,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { tags, ports, schemes, locations } = overviewFilters;
|
||||||
filters: { tags, ports, schemes, locations },
|
const selectedPorts = alertParams?.filters?.['url.port'] ?? [];
|
||||||
} = useSelector(overviewFiltersSelector);
|
const selectedLocations = alertParams?.filters?.['observer.geo.name'] ?? [];
|
||||||
|
const selectedSchemes = alertParams?.filters?.['monitor.type'] ?? [];
|
||||||
const [updatedFieldValues, setUpdatedFieldValues] = useState<{
|
const selectedTags = alertParams?.filters?.tags ?? [];
|
||||||
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 onFilterFieldChange = (fieldName: string, values: string[]) => {
|
const onFilterFieldChange = (fieldName: string, values: string[]) => {
|
||||||
setFilters({
|
// the `filters` field is no longer a string
|
||||||
...filters,
|
if (alertParams.filters && typeof alertParams.filters !== 'string') {
|
||||||
[fieldName]: values,
|
setAlertParams('filters', { ...alertParams.filters, [fieldName]: values });
|
||||||
});
|
} else {
|
||||||
|
setAlertParams(
|
||||||
|
'filters',
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
tags: [],
|
||||||
|
'url.port': [],
|
||||||
|
'observer.geo.name': [],
|
||||||
|
'monitor.type': [],
|
||||||
|
},
|
||||||
|
{ [fieldName]: values }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
setUpdatedFieldValues({ fieldName, values });
|
setUpdatedFieldValues({ fieldName, values });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,4 +6,5 @@
|
||||||
|
|
||||||
export { DownNoExpressionSelect } from './down_number_select';
|
export { DownNoExpressionSelect } from './down_number_select';
|
||||||
export { FiltersExpressionsSelect } from './filters_expression_select';
|
export { FiltersExpressionsSelect } from './filters_expression_select';
|
||||||
|
export { FiltersExpressionSelectContainer } from './filters_expression_select_container';
|
||||||
export { TimeExpressionSelect } from './time_expression_select';
|
export { TimeExpressionSelect } from './time_expression_select';
|
||||||
|
|
|
@ -13,9 +13,13 @@ import { AlertFieldNumber } from '../alert_field_number';
|
||||||
import { timeExpLabels } from './translations';
|
import { timeExpLabels } from './translations';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
defaultTimerangeCount?: number;
|
||||||
|
defaultTimerangeUnit?: string;
|
||||||
setAlertParams: (key: string, value: any) => void;
|
setAlertParams: (key: string, value: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_TIMERANGE_UNIT = 'm';
|
||||||
|
|
||||||
const TimeRangeOptions = [
|
const TimeRangeOptions = [
|
||||||
{
|
{
|
||||||
'aria-label': labels.SECONDS_TIME_RANGE,
|
'aria-label': labels.SECONDS_TIME_RANGE,
|
||||||
|
@ -26,7 +30,6 @@ const TimeRangeOptions = [
|
||||||
{
|
{
|
||||||
'aria-label': labels.MINUTES_TIME_RANGE,
|
'aria-label': labels.MINUTES_TIME_RANGE,
|
||||||
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption',
|
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption',
|
||||||
checked: 'on',
|
|
||||||
key: 'm',
|
key: 'm',
|
||||||
label: labels.MINUTES,
|
label: labels.MINUTES,
|
||||||
},
|
},
|
||||||
|
@ -44,10 +47,18 @@ const TimeRangeOptions = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TimeExpressionSelect: React.FC<Props> = ({ setAlertParams }) => {
|
export const TimeExpressionSelect: React.FC<Props> = ({
|
||||||
const [numUnits, setNumUnits] = useState<number>(15);
|
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(() => {
|
useEffect(() => {
|
||||||
const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm';
|
const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm';
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,11 +4,11 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 { EuiExpression, EuiPopover } from '@elastic/eui';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { SETTINGS_ROUTE } from '../../../../common/constants';
|
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||||
|
|
||||||
interface SettingsMessageExpressionPopoverProps {
|
interface SettingsMessageExpressionPopoverProps {
|
||||||
'aria-label': string;
|
'aria-label': string;
|
||||||
|
@ -25,9 +25,12 @@ export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressio
|
||||||
value,
|
value,
|
||||||
id,
|
id,
|
||||||
}) => {
|
}) => {
|
||||||
|
const kibana = useKibana();
|
||||||
|
const path = kibana.services?.application?.getUrlForApp('uptime', { path: 'settings' });
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<EuiPopover
|
<EuiPopover
|
||||||
|
data-test-subj={`xpack.uptime.alerts.tls.expressionPopover.${id}`}
|
||||||
id={id}
|
id={id}
|
||||||
anchorPosition="downLeft"
|
anchorPosition="downLeft"
|
||||||
button={
|
button={
|
||||||
|
@ -50,7 +53,7 @@ export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressio
|
||||||
settingsPageLink: (
|
settingsPageLink: (
|
||||||
// this link is wrapped around a span so we can also change the UI state
|
// 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
|
// 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
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAlertFlyoutVisible(false);
|
setAlertFlyoutVisible(false);
|
||||||
|
@ -63,7 +66,7 @@ export const SettingsMessageExpressionPopover: React.FC<SettingsMessageExpressio
|
||||||
>
|
>
|
||||||
settings page
|
settings page
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</EuiLink>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -36,13 +36,19 @@ function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
|
||||||
interface Props {
|
interface Props {
|
||||||
'aria-label': string;
|
'aria-label': string;
|
||||||
autocomplete: DataPublicPluginSetup['autocomplete'];
|
autocomplete: DataPublicPluginSetup['autocomplete'];
|
||||||
|
defaultKuery?: string;
|
||||||
'data-test-subj': string;
|
'data-test-subj': string;
|
||||||
|
shouldUpdateUrl?: boolean;
|
||||||
|
updateDefaultKuery?: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function KueryBar({
|
export function KueryBar({
|
||||||
'aria-label': ariaLabel,
|
'aria-label': ariaLabel,
|
||||||
autocomplete: autocompleteService,
|
autocomplete: autocompleteService,
|
||||||
|
defaultKuery,
|
||||||
'data-test-subj': dataTestSubj,
|
'data-test-subj': dataTestSubj,
|
||||||
|
shouldUpdateUrl,
|
||||||
|
updateDefaultKuery,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { loading, index_pattern: indexPattern } = useIndexPattern();
|
const { loading, index_pattern: indexPattern } = useIndexPattern();
|
||||||
const { updateSearchText } = useSearchText();
|
const { updateSearchText } = useSearchText();
|
||||||
|
@ -68,8 +74,6 @@ export function KueryBar({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSearchText(inputValue);
|
|
||||||
|
|
||||||
setIsLoadingSuggestions(true);
|
setIsLoadingSuggestions(true);
|
||||||
setState({ ...state, suggestions: [] });
|
setState({ ...state, suggestions: [] });
|
||||||
|
|
||||||
|
@ -112,7 +116,13 @@ export function KueryBar({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrlParams({ search: inputValue.trim() });
|
if (shouldUpdateUrl !== false) {
|
||||||
|
updateUrlParams({ search: inputValue.trim() });
|
||||||
|
}
|
||||||
|
updateSearchText(inputValue);
|
||||||
|
if (updateDefaultKuery) {
|
||||||
|
updateDefaultKuery(inputValue);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
|
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
@ -125,7 +135,7 @@ export function KueryBar({
|
||||||
data-test-subj={dataTestSubj}
|
data-test-subj={dataTestSubj}
|
||||||
disabled={indexPatternMissing}
|
disabled={indexPatternMissing}
|
||||||
isLoading={isLoadingSuggestions || loading}
|
isLoading={isLoadingSuggestions || loading}
|
||||||
initialValue={kuery}
|
initialValue={defaultKuery || kuery}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
suggestions={state.suggestions}
|
suggestions={state.suggestions}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FilterStatusButton, FilterStatusButtonProps } from '../filter_status_button';
|
import { FilterStatusButton, FilterStatusButtonProps } from '../filter_status_button';
|
||||||
import { renderWithRouter, shallowWithRouter } from '../../../../lib';
|
import { renderWithRouter, shallowWithRouter, MountWithReduxProvider } from '../../../../lib';
|
||||||
|
|
||||||
describe('FilterStatusButton', () => {
|
describe('FilterStatusButton', () => {
|
||||||
let props: FilterStatusButtonProps;
|
let props: FilterStatusButtonProps;
|
||||||
|
@ -26,7 +26,11 @@ describe('FilterStatusButton', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders without errors for valid props', () => {
|
it('renders without errors for valid props', () => {
|
||||||
const wrapper = renderWithRouter(<FilterStatusButton {...props} />);
|
const wrapper = renderWithRouter(
|
||||||
|
<MountWithReduxProvider>
|
||||||
|
<FilterStatusButton {...props} />
|
||||||
|
</MountWithReduxProvider>
|
||||||
|
);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,7 +5,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithRouter, renderWithRouter, shallowWithRouter } from '../../../../lib';
|
import {
|
||||||
|
mountWithRouter,
|
||||||
|
renderWithRouter,
|
||||||
|
shallowWithRouter,
|
||||||
|
MountWithReduxProvider,
|
||||||
|
} from '../../../../lib';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { StatusFilter } from '../status_filter';
|
import { StatusFilter } from '../status_filter';
|
||||||
import { FilterStatusButton } from '../filter_status_button';
|
import { FilterStatusButton } from '../filter_status_button';
|
||||||
|
@ -18,7 +23,12 @@ describe('StatusFilterComponent', () => {
|
||||||
initialEntries: [`/?g=%22%22&statusFilter=${status}`],
|
initialEntries: [`/?g=%22%22&statusFilter=${status}`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = mountWithRouter(<StatusFilter />, history);
|
const wrapper = mountWithRouter(
|
||||||
|
<MountWithReduxProvider>
|
||||||
|
<StatusFilter />
|
||||||
|
</MountWithReduxProvider>,
|
||||||
|
history
|
||||||
|
);
|
||||||
const filterBtns = wrapper.find(FilterStatusButton);
|
const filterBtns = wrapper.find(FilterStatusButton);
|
||||||
|
|
||||||
const allBtn = filterBtns.at(0);
|
const allBtn = filterBtns.at(0);
|
||||||
|
@ -34,7 +44,11 @@ describe('StatusFilterComponent', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders without errors for valid props', () => {
|
it('renders without errors for valid props', () => {
|
||||||
const wrapper = renderWithRouter(<StatusFilter />);
|
const wrapper = renderWithRouter(
|
||||||
|
<MountWithReduxProvider>
|
||||||
|
<StatusFilter />
|
||||||
|
</MountWithReduxProvider>
|
||||||
|
);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -137,30 +137,95 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UseUrlParamsTestComponent
|
<MountWithReduxProvider>
|
||||||
hook={[Function]}
|
<Provider
|
||||||
updateParams={
|
store={
|
||||||
Object {
|
Object {
|
||||||
"pagination": "",
|
"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
|
<UseUrlParamsTestComponent
|
||||||
</button>
|
hook={[Function]}
|
||||||
<button
|
updateParams={
|
||||||
id="getUrlParams"
|
Object {
|
||||||
onClick={[Function]}
|
"pagination": "",
|
||||||
>
|
}
|
||||||
Get url params
|
}
|
||||||
</button>
|
>
|
||||||
</UseUrlParamsTestComponent>
|
<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>
|
</Router>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -301,24 +366,89 @@ exports[`useUrlParams gets the expected values using the context 1`] = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<UseUrlParamsTestComponent
|
<MountWithReduxProvider>
|
||||||
hook={[Function]}
|
<Provider
|
||||||
>
|
store={
|
||||||
<div>
|
Object {
|
||||||
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":""}
|
"dispatch": [MockFunction],
|
||||||
</div>
|
"getState": [MockFunction] {
|
||||||
<button
|
"calls": Array [
|
||||||
id="setUrlParams"
|
Array [],
|
||||||
onClick={[Function]}
|
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
|
<UseUrlParamsTestComponent
|
||||||
</button>
|
hook={[Function]}
|
||||||
<button
|
>
|
||||||
id="getUrlParams"
|
<div>
|
||||||
onClick={[Function]}
|
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","selectedPingStatus":"","statusFilter":""}
|
||||||
>
|
</div>
|
||||||
Get url params
|
<button
|
||||||
</button>
|
id="setUrlParams"
|
||||||
</UseUrlParamsTestComponent>
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Set url params
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="getUrlParams"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Get url params
|
||||||
|
</button>
|
||||||
|
</UseUrlParamsTestComponent>
|
||||||
|
</Provider>
|
||||||
|
</MountWithReduxProvider>
|
||||||
</Router>
|
</Router>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { Route } from 'react-router-dom';
|
||||||
import { mountWithRouter } from '../../lib';
|
import { mountWithRouter } from '../../lib';
|
||||||
import { OVERVIEW_ROUTE } from '../../../common/constants';
|
import { OVERVIEW_ROUTE } from '../../../common/constants';
|
||||||
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
|
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';
|
import { makeBaseBreadcrumb, useBreadcrumbs } from '../use_breadcrumbs';
|
||||||
|
|
||||||
describe('useBreadcrumbs', () => {
|
describe('useBreadcrumbs', () => {
|
||||||
|
@ -34,11 +34,13 @@ describe('useBreadcrumbs', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
mountWithRouter(
|
mountWithRouter(
|
||||||
<KibanaContextProvider services={{ ...core }}>
|
<MountWithReduxProvider>
|
||||||
<Route path={OVERVIEW_ROUTE}>
|
<KibanaContextProvider services={{ ...core }}>
|
||||||
<Component />
|
<Route path={OVERVIEW_ROUTE}>
|
||||||
</Route>
|
<Component />
|
||||||
</KibanaContextProvider>
|
</Route>
|
||||||
|
</KibanaContextProvider>
|
||||||
|
</MountWithReduxProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const urlParams: UptimeUrlParams = getSupportedUrlParams({});
|
const urlParams: UptimeUrlParams = getSupportedUrlParams({});
|
||||||
|
|
|
@ -8,7 +8,7 @@ import DateMath from '@elastic/datemath';
|
||||||
import React, { useState, Fragment } from 'react';
|
import React, { useState, Fragment } from 'react';
|
||||||
import { useUrlParams, UptimeUrlParamsHook } from '../use_url_params';
|
import { useUrlParams, UptimeUrlParamsHook } from '../use_url_params';
|
||||||
import { UptimeRefreshContext } from '../../contexts';
|
import { UptimeRefreshContext } from '../../contexts';
|
||||||
import { mountWithRouter } from '../../lib';
|
import { mountWithRouter, MountWithReduxProvider } from '../../lib';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
|
|
||||||
interface MockUrlParamsComponentProps {
|
interface MockUrlParamsComponentProps {
|
||||||
|
@ -52,9 +52,11 @@ describe('useUrlParams', () => {
|
||||||
jest.spyOn(history, 'push');
|
jest.spyOn(history, 'push');
|
||||||
|
|
||||||
const component = mountWithRouter(
|
const component = mountWithRouter(
|
||||||
<UptimeRefreshContext.Provider value={{ lastRefresh: 123, refreshApp: jest.fn() }}>
|
<MountWithReduxProvider>
|
||||||
<UseUrlParamsTestComponent hook={useUrlParams} />
|
<UptimeRefreshContext.Provider value={{ lastRefresh: 123, refreshApp: jest.fn() }}>
|
||||||
</UptimeRefreshContext.Provider>,
|
<UseUrlParamsTestComponent hook={useUrlParams} />
|
||||||
|
</UptimeRefreshContext.Provider>
|
||||||
|
</MountWithReduxProvider>,
|
||||||
history
|
history
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -68,14 +70,16 @@ describe('useUrlParams', () => {
|
||||||
|
|
||||||
it('gets the expected values using the context', () => {
|
it('gets the expected values using the context', () => {
|
||||||
const component = mountWithRouter(
|
const component = mountWithRouter(
|
||||||
<UptimeRefreshContext.Provider
|
<MountWithReduxProvider>
|
||||||
value={{
|
<UptimeRefreshContext.Provider
|
||||||
lastRefresh: 123,
|
value={{
|
||||||
refreshApp: jest.fn(),
|
lastRefresh: 123,
|
||||||
}}
|
refreshApp: jest.fn(),
|
||||||
>
|
}}
|
||||||
<UseUrlParamsTestComponent hook={useUrlParams} />
|
>
|
||||||
</UptimeRefreshContext.Provider>
|
<UseUrlParamsTestComponent hook={useUrlParams} />
|
||||||
|
</UptimeRefreshContext.Provider>
|
||||||
|
</MountWithReduxProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const getUrlParamsButton = component.find('#getUrlParams');
|
const getUrlParamsButton = component.find('#getUrlParams');
|
||||||
|
@ -92,14 +96,16 @@ describe('useUrlParams', () => {
|
||||||
|
|
||||||
jest.spyOn(history, 'push');
|
jest.spyOn(history, 'push');
|
||||||
const component = mountWithRouter(
|
const component = mountWithRouter(
|
||||||
<UptimeRefreshContext.Provider
|
<MountWithReduxProvider>
|
||||||
value={{
|
<UptimeRefreshContext.Provider
|
||||||
lastRefresh: 123,
|
value={{
|
||||||
refreshApp: jest.fn(),
|
lastRefresh: 123,
|
||||||
}}
|
refreshApp: jest.fn(),
|
||||||
>
|
}}
|
||||||
<UseUrlParamsTestComponent hook={useUrlParams} updateParams={{ pagination: '' }} />
|
>
|
||||||
</UptimeRefreshContext.Provider>,
|
<UseUrlParamsTestComponent hook={useUrlParams} updateParams={{ pagination: '' }} />
|
||||||
|
</UptimeRefreshContext.Provider>
|
||||||
|
</MountWithReduxProvider>,
|
||||||
history
|
history
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,18 @@ interface SelectedFilters {
|
||||||
selectedFilters: Map<string, string[]>;
|
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 [getUrlParams, updateUrl] = useUrlParams();
|
||||||
|
|
||||||
const { filters: currentFilters } = getUrlParams();
|
const { filters: currentFilters } = getUrlParams();
|
||||||
|
|
||||||
// update filters in the URL from filter group
|
// update filters in the URL from filter group
|
||||||
const onFilterUpdate = (filtersKuery: string) => {
|
const onFilterUpdate = (filtersKuery: string) => {
|
||||||
if (currentFilters !== filtersKuery) {
|
if (currentFilters !== filtersKuery && shouldUpdateUrl) {
|
||||||
updateUrl({ filters: filtersKuery, pagination: '' });
|
updateUrl({ filters: filtersKuery, pagination: '' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,9 +4,13 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { parse, stringify } from 'query-string';
|
import { parse, stringify } from 'query-string';
|
||||||
import { useLocation, useHistory } from 'react-router-dom';
|
import { useLocation, useHistory } from 'react-router-dom';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper';
|
import { UptimeUrlParams, getSupportedUrlParams } from '../lib/helper';
|
||||||
|
import { selectedFiltersSelector } from '../state/selectors';
|
||||||
|
import { setSelectedFilters } from '../state/actions/selected_filters';
|
||||||
|
|
||||||
export type GetUrlParams = () => UptimeUrlParams;
|
export type GetUrlParams = () => UptimeUrlParams;
|
||||||
export type UpdateUrlParams = (updatedParams: {
|
export type UpdateUrlParams = (updatedParams: {
|
||||||
|
@ -27,9 +31,35 @@ export const useGetUrlParams: GetUrlParams = () => {
|
||||||
return getSupportedUrlParams(params);
|
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 = () => {
|
export const useUrlParams: UptimeUrlParamsHook = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
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) => {
|
const updateUrlParams: UpdateUrlParams = (updatedParams) => {
|
||||||
if (!history || !location) return;
|
if (!history || !location) return;
|
||||||
|
@ -57,6 +87,12 @@ export const useUrlParams: UptimeUrlParamsHook = () => {
|
||||||
{ sort: false }
|
{ sort: false }
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
const filterMap = getMapFromFilters(mergedParams.filters);
|
||||||
|
if (!filterMap) {
|
||||||
|
dispatch(setSelectedFilters(null));
|
||||||
|
} else {
|
||||||
|
dispatch(setSelectedFilters(mapMapToObject(filterMap)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return [useGetUrlParams, updateUrlParams];
|
return [useGetUrlParams, updateUrlParams];
|
||||||
|
|
|
@ -9,7 +9,6 @@ import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { i18n as i18nFormatter } from '@kbn/i18n';
|
import { i18n as i18nFormatter } from '@kbn/i18n';
|
||||||
import { alertTypeInitializers } from '../../alert_types';
|
|
||||||
import { UptimeApp, UptimeAppProps } from '../../../uptime_app';
|
import { UptimeApp, UptimeAppProps } from '../../../uptime_app';
|
||||||
import { getIntegratedAppAvailability } from './capabilities_adapter';
|
import { getIntegratedAppAvailability } from './capabilities_adapter';
|
||||||
import {
|
import {
|
||||||
|
@ -34,18 +33,6 @@ export const getKibanaFrameworkAdapter = (
|
||||||
i18n,
|
i18n,
|
||||||
} = core;
|
} = 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(
|
const { apm, infrastructure, logs } = getIntegratedAppAvailability(
|
||||||
capabilities,
|
capabilities,
|
||||||
INTEGRATED_SOLUTIONS
|
INTEGRATED_SOLUTIONS
|
||||||
|
|
|
@ -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', () => {
|
describe('timerange', () => {
|
||||||
it('has invalid timerangeCount value', () => {
|
it('has invalid timerangeCount value', () => {
|
||||||
expect(validate({ ...params, timerangeCount: 0 })).toMatchInlineSnapshot(`
|
expect(validate({ ...params, timerangeCount: 0 })).toMatchInlineSnapshot(`
|
||||||
|
@ -96,7 +114,22 @@ describe('monitor status alert type', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initMonitorStatusAlertType', () => {
|
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 {
|
Object {
|
||||||
"alertParamsExpression": [Function],
|
"alertParamsExpression": [Function],
|
||||||
"defaultActionMessage": "{{context.message}}
|
"defaultActionMessage": "{{context.message}}
|
||||||
|
@ -104,8 +137,20 @@ describe('monitor status alert type', () => {
|
||||||
{{context.downMonitorsWithGeo}}",
|
{{context.downMonitorsWithGeo}}",
|
||||||
"iconClass": "uptimeApp",
|
"iconClass": "uptimeApp",
|
||||||
"id": "xpack.uptime.alerts.monitorStatus",
|
"id": "xpack.uptime.alerts.monitorStatus",
|
||||||
"name": <MonitorStatusTitle />,
|
"name": <Provider
|
||||||
"requiresAppContext": true,
|
store={
|
||||||
|
Object {
|
||||||
|
"dispatch": [Function],
|
||||||
|
"getState": [Function],
|
||||||
|
"replaceReducer": [Function],
|
||||||
|
"subscribe": [Function],
|
||||||
|
Symbol(observable): [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MonitorStatusTitle />
|
||||||
|
</Provider>,
|
||||||
|
"requiresAppContext": false,
|
||||||
"validate": [Function],
|
"validate": [Function],
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -4,11 +4,16 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||||
import { initMonitorStatusAlertType } from './monitor_status';
|
import { initMonitorStatusAlertType } from './monitor_status';
|
||||||
import { initTlsAlertType } from './tls';
|
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[] = [
|
export const alertTypeInitializers: AlertTypeInitializer[] = [
|
||||||
initMonitorStatusAlertType,
|
initMonitorStatusAlertType,
|
||||||
|
|
|
@ -4,24 +4,33 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 React from 'react';
|
||||||
import { isRight } from 'fp-ts/lib/Either';
|
import { isRight } from 'fp-ts/lib/Either';
|
||||||
import { PathReporter } from 'io-ts/lib/PathReporter';
|
import { PathReporter } from 'io-ts/lib/PathReporter';
|
||||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||||
import { AlertTypeInitializer } from '.';
|
import { AlertTypeInitializer } from '.';
|
||||||
import { AtomicStatusCheckParamsType } from '../../../common/runtime_types';
|
import { AtomicStatusCheckParamsType, StatusCheckParamsType } from '../../../common/runtime_types';
|
||||||
import { MonitorStatusTitle } from './monitor_status_title';
|
import { MonitorStatusTitle } from './monitor_status_title';
|
||||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
|
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
|
||||||
import { MonitorStatusTranslations } from './translations';
|
import { MonitorStatusTranslations } from './translations';
|
||||||
|
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
|
||||||
|
import { store } from '../../state';
|
||||||
|
|
||||||
export const validate = (alertParams: unknown) => {
|
export const validate = (alertParams: unknown) => {
|
||||||
const errors: Record<string, any> = {};
|
const errors: Record<string, any> = {};
|
||||||
const decoded = AtomicStatusCheckParamsType.decode(alertParams);
|
const decoded = AtomicStatusCheckParamsType.decode(alertParams);
|
||||||
|
const oldDecoded = StatusCheckParamsType.decode(alertParams);
|
||||||
|
|
||||||
if (!isRight(decoded)) {
|
if (!isRight(decoded) && !isRight(oldDecoded)) {
|
||||||
errors.typeCheckFailure = 'Provided parameters do not conform to the expected type.';
|
return {
|
||||||
errors.typeCheckParsingMessage = PathReporter.report(decoded);
|
errors: {
|
||||||
} else {
|
typeCheckFailure: 'Provided parameters do not conform to the expected type.',
|
||||||
|
typeCheckParsingMessage: PathReporter.report(decoded),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isRight(decoded)) {
|
||||||
const { numTimes, timerangeCount } = decoded.right;
|
const { numTimes, timerangeCount } = decoded.right;
|
||||||
if (numTimes < 1) {
|
if (numTimes < 1) {
|
||||||
errors.invalidNumTimes = 'Number of alert check down times must be an integer greater than 0';
|
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 = ({
|
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
|
||||||
autocomplete,
|
core,
|
||||||
|
plugins,
|
||||||
}): AlertTypeModel => ({
|
}): AlertTypeModel => ({
|
||||||
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
|
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
|
||||||
name: <MonitorStatusTitle />,
|
name: (
|
||||||
iconClass: 'uptimeApp',
|
<ReduxProvider store={store}>
|
||||||
alertParamsExpression: (params: any) => (
|
<MonitorStatusTitle />
|
||||||
<AlertMonitorStatus {...params} autocomplete={autocomplete} />
|
</ReduxProvider>
|
||||||
),
|
),
|
||||||
|
iconClass: 'uptimeApp',
|
||||||
|
alertParamsExpression: (params: any) => {
|
||||||
|
return (
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||||
|
<AlertMonitorStatus {...params} autocomplete={plugins.data.autocomplete} />
|
||||||
|
</KibanaContextProvider>
|
||||||
|
</ReduxProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
validate,
|
validate,
|
||||||
defaultActionMessage,
|
defaultActionMessage,
|
||||||
requiresAppContext: true,
|
requiresAppContext: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,30 +5,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui';
|
|
||||||
import { snapshotDataSelector } from '../../state/selectors';
|
|
||||||
|
|
||||||
export const MonitorStatusTitle = () => {
|
export const MonitorStatusTitle = () => {
|
||||||
const { count, loading } = useSelector(snapshotDataSelector);
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup>
|
<FormattedMessage
|
||||||
<EuiFlexItem>
|
id="xpack.uptime.alerts.monitorStatus.title.label"
|
||||||
<FormattedMessage
|
defaultMessage="Uptime monitor status"
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,21 +5,30 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
|
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
|
||||||
import { TlsTranslations } from './translations';
|
import { TlsTranslations } from './translations';
|
||||||
import { AlertTypeInitializer } from '.';
|
import { AlertTypeInitializer } from '.';
|
||||||
|
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
|
||||||
|
import { store } from '../../state';
|
||||||
|
|
||||||
const { name, defaultActionMessage } = TlsTranslations;
|
const { name, defaultActionMessage } = TlsTranslations;
|
||||||
|
const TlsAlertExpression = React.lazy(() =>
|
||||||
export const initTlsAlertType: AlertTypeInitializer = (): AlertTypeModel => ({
|
import('../../components/overview/alerts/alerts_containers/alert_tls')
|
||||||
|
);
|
||||||
|
export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({
|
||||||
id: CLIENT_ALERT_TYPES.TLS,
|
id: CLIENT_ALERT_TYPES.TLS,
|
||||||
iconClass: 'uptimeApp',
|
iconClass: 'uptimeApp',
|
||||||
alertParamsExpression: React.lazy(() =>
|
alertParamsExpression: (_params: any) => (
|
||||||
import('../../components/overview/alerts/alerts_containers/alert_tls')
|
<ReduxProvider store={store}>
|
||||||
|
<KibanaContextProvider services={{ ...core, ...plugins }}>
|
||||||
|
<TlsAlertExpression />
|
||||||
|
</KibanaContextProvider>
|
||||||
|
</ReduxProvider>
|
||||||
),
|
),
|
||||||
name,
|
name,
|
||||||
validate: () => ({ errors: {} }),
|
validate: () => ({ errors: {} }),
|
||||||
defaultActionMessage,
|
defaultActionMessage,
|
||||||
requiresAppContext: true,
|
requiresAppContext: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
|
@ -9,3 +9,4 @@ export * from './observability_integration';
|
||||||
export { getChartDateLabel } from './charts';
|
export { getChartDateLabel } from './charts';
|
||||||
export { seriesHasDownValues } from './series_has_down_values';
|
export { seriesHasDownValues } from './series_has_down_values';
|
||||||
export { UptimeUrlParams, getSupportedUrlParams } from './url_params';
|
export { UptimeUrlParams, getSupportedUrlParams } from './url_params';
|
||||||
|
export { MountWithReduxProvider } from './helper_with_redux';
|
||||||
|
|
|
@ -4,4 +4,5 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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';
|
export { renderWithRouter, shallowWithRouter, mountWithRouter } from './helper/helper_with_router';
|
||||||
|
|
|
@ -6,47 +6,33 @@
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PageHeader } from '../page_header';
|
import { PageHeader } from '../page_header';
|
||||||
import { renderWithRouter } from '../../lib';
|
import { renderWithRouter, MountWithReduxProvider } from '../../lib';
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
|
|
||||||
describe('PageHeader', () => {
|
describe('PageHeader', () => {
|
||||||
it('shallow renders with the date picker', () => {
|
it('shallow renders with the date picker', () => {
|
||||||
const component = renderWithRouter(
|
const component = renderWithRouter(
|
||||||
<MockReduxProvider>
|
<MountWithReduxProvider>
|
||||||
<PageHeader headingText={'TestingHeading'} datePicker={true} />
|
<PageHeader headingText={'TestingHeading'} datePicker={true} />
|
||||||
</MockReduxProvider>
|
</MountWithReduxProvider>
|
||||||
);
|
);
|
||||||
expect(component).toMatchSnapshot('page_header_with_date_picker');
|
expect(component).toMatchSnapshot('page_header_with_date_picker');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shallow renders without the date picker', () => {
|
it('shallow renders without the date picker', () => {
|
||||||
const component = renderWithRouter(
|
const component = renderWithRouter(
|
||||||
<MockReduxProvider>
|
<MountWithReduxProvider>
|
||||||
<PageHeader headingText={'TestingHeading'} datePicker={false} />
|
<PageHeader headingText={'TestingHeading'} datePicker={false} />
|
||||||
</MockReduxProvider>
|
</MountWithReduxProvider>
|
||||||
);
|
);
|
||||||
expect(component).toMatchSnapshot('page_header_no_date_picker');
|
expect(component).toMatchSnapshot('page_header_no_date_picker');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shallow renders extra links', () => {
|
it('shallow renders extra links', () => {
|
||||||
const component = renderWithRouter(
|
const component = renderWithRouter(
|
||||||
<MockReduxProvider>
|
<MountWithReduxProvider>
|
||||||
<PageHeader headingText={'TestingHeading'} extraLinks={true} datePicker={true} />
|
<PageHeader headingText={'TestingHeading'} extraLinks={true} datePicker={true} />
|
||||||
</MockReduxProvider>
|
</MountWithReduxProvider>
|
||||||
);
|
);
|
||||||
expect(component).toMatchSnapshot('page_header_with_extra_links');
|
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>
|
|
||||||
);
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { OverviewFilters } from '../../../common/runtime_types';
|
||||||
export const FETCH_OVERVIEW_FILTERS = 'FETCH_OVERVIEW_FILTERS';
|
export const FETCH_OVERVIEW_FILTERS = 'FETCH_OVERVIEW_FILTERS';
|
||||||
export const FETCH_OVERVIEW_FILTERS_FAIL = 'FETCH_OVERVIEW_FILTERS_FAIL';
|
export const FETCH_OVERVIEW_FILTERS_FAIL = 'FETCH_OVERVIEW_FILTERS_FAIL';
|
||||||
export const FETCH_OVERVIEW_FILTERS_SUCCESS = 'FETCH_OVERVIEW_FILTERS_SUCCESS';
|
export const FETCH_OVERVIEW_FILTERS_SUCCESS = 'FETCH_OVERVIEW_FILTERS_SUCCESS';
|
||||||
|
export const SET_OVERVIEW_FILTERS = 'SET_OVERVIEW_FILTERS';
|
||||||
|
|
||||||
export interface GetOverviewFiltersPayload {
|
export interface GetOverviewFiltersPayload {
|
||||||
dateRangeStart: string;
|
dateRangeStart: string;
|
||||||
|
@ -36,10 +37,16 @@ interface GetOverviewFiltersFailAction {
|
||||||
payload: Error;
|
payload: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SetOverviewFiltersAction {
|
||||||
|
type: typeof SET_OVERVIEW_FILTERS;
|
||||||
|
payload: OverviewFilters;
|
||||||
|
}
|
||||||
|
|
||||||
export type OverviewFiltersAction =
|
export type OverviewFiltersAction =
|
||||||
| GetOverviewFiltersFetchAction
|
| GetOverviewFiltersFetchAction
|
||||||
| GetOverviewFiltersSuccessAction
|
| GetOverviewFiltersSuccessAction
|
||||||
| GetOverviewFiltersFailAction;
|
| GetOverviewFiltersFailAction
|
||||||
|
| SetOverviewFiltersAction;
|
||||||
|
|
||||||
export const fetchOverviewFilters = (
|
export const fetchOverviewFilters = (
|
||||||
payload: GetOverviewFiltersPayload
|
payload: GetOverviewFiltersPayload
|
||||||
|
@ -59,3 +66,8 @@ export const fetchOverviewFiltersSuccess = (
|
||||||
type: FETCH_OVERVIEW_FILTERS_SUCCESS,
|
type: FETCH_OVERVIEW_FILTERS_SUCCESS,
|
||||||
payload: filters,
|
payload: filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setOverviewFilters = (filters: OverviewFilters): SetOverviewFiltersAction => ({
|
||||||
|
type: SET_OVERVIEW_FILTERS,
|
||||||
|
payload: filters,
|
||||||
|
});
|
||||||
|
|
|
@ -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'
|
||||||
|
);
|
|
@ -9,12 +9,12 @@ import createSagaMiddleware from 'redux-saga';
|
||||||
import { rootEffect } from './effects';
|
import { rootEffect } from './effects';
|
||||||
import { rootReducer } from './reducers';
|
import { rootReducer } from './reducers';
|
||||||
|
|
||||||
|
export type AppState = ReturnType<typeof rootReducer>;
|
||||||
|
|
||||||
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
|
||||||
const sagaMW = createSagaMiddleware();
|
const sagaMW = createSagaMiddleware();
|
||||||
|
|
||||||
export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMW)));
|
export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMW)));
|
||||||
|
|
||||||
export type AppState = ReturnType<typeof rootReducer>;
|
|
||||||
|
|
||||||
sagaMW.run(rootEffect);
|
sagaMW.run(rootEffect);
|
||||||
|
|
|
@ -4,7 +4,12 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 { uiReducer } from '../ui';
|
||||||
import { Action } from 'redux-actions';
|
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",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { monitorDurationReducer } from './monitor_duration';
|
||||||
import { indexStatusReducer } from './index_status';
|
import { indexStatusReducer } from './index_status';
|
||||||
import { mlJobsReducer } from './ml_anomaly';
|
import { mlJobsReducer } from './ml_anomaly';
|
||||||
import { certificatesReducer } from '../certificates/certificates';
|
import { certificatesReducer } from '../certificates/certificates';
|
||||||
|
import { selectedFiltersReducer } from './selected_filters';
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
monitor: monitorReducer,
|
monitor: monitorReducer,
|
||||||
|
@ -35,4 +36,5 @@ export const rootReducer = combineReducers({
|
||||||
monitorDuration: monitorDurationReducer,
|
monitorDuration: monitorDurationReducer,
|
||||||
indexStatus: indexStatusReducer,
|
indexStatus: indexStatusReducer,
|
||||||
certificates: certificatesReducer,
|
certificates: certificatesReducer,
|
||||||
|
selectedFilters: selectedFiltersReducer,
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
FETCH_OVERVIEW_FILTERS_FAIL,
|
FETCH_OVERVIEW_FILTERS_FAIL,
|
||||||
FETCH_OVERVIEW_FILTERS_SUCCESS,
|
FETCH_OVERVIEW_FILTERS_SUCCESS,
|
||||||
OverviewFiltersAction,
|
OverviewFiltersAction,
|
||||||
|
SET_OVERVIEW_FILTERS,
|
||||||
} from '../actions';
|
} from '../actions';
|
||||||
|
|
||||||
export interface OverviewFiltersState {
|
export interface OverviewFiltersState {
|
||||||
|
@ -51,6 +52,11 @@ export function overviewFiltersReducer(
|
||||||
errors: [...state.errors, action.payload],
|
errors: [...state.errors, action.payload],
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
|
case SET_OVERVIEW_FILTERS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: action.payload,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -107,6 +107,7 @@ describe('state selectors', () => {
|
||||||
loading: false,
|
loading: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
selectedFilters: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
it('selects base path from state', () => {
|
it('selects base path from state', () => {
|
||||||
|
|
|
@ -86,3 +86,5 @@ export const overviewFiltersSelector = ({ overviewFilters }: AppState) => overvi
|
||||||
export const esKuerySelector = ({ ui: { esKuery } }: AppState) => esKuery;
|
export const esKuerySelector = ({ ui: { esKuery } }: AppState) => esKuery;
|
||||||
|
|
||||||
export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchText;
|
export const searchTextSelector = ({ ui: { searchText } }: AppState) => searchText;
|
||||||
|
|
||||||
|
export const selectedFiltersSelector = ({ selectedFilters }: AppState) => selectedFilters;
|
||||||
|
|
|
@ -20,14 +20,13 @@ import {
|
||||||
UptimeStartupPluginsContextProvider,
|
UptimeStartupPluginsContextProvider,
|
||||||
} from './contexts';
|
} from './contexts';
|
||||||
import { CommonlyUsedRange } from './components/common/uptime_date_picker';
|
import { CommonlyUsedRange } from './components/common/uptime_date_picker';
|
||||||
import { store } from './state';
|
|
||||||
import { setBasePath } from './state/actions';
|
import { setBasePath } from './state/actions';
|
||||||
import { PageRouter } from './routes';
|
import { PageRouter } from './routes';
|
||||||
import {
|
import {
|
||||||
UptimeAlertsContextProvider,
|
UptimeAlertsContextProvider,
|
||||||
UptimeAlertsFlyoutWrapper,
|
UptimeAlertsFlyoutWrapper,
|
||||||
} from './components/overview/alerts';
|
} from './components/overview/alerts';
|
||||||
import { kibanaService } from './state/kibana_service';
|
import { store } from './state';
|
||||||
|
|
||||||
export interface UptimeAppColors {
|
export interface UptimeAppColors {
|
||||||
danger: string;
|
danger: string;
|
||||||
|
@ -87,8 +86,6 @@ const Application = (props: UptimeAppProps) => {
|
||||||
);
|
);
|
||||||
}, [canSave, renderGlobalHelpControls, setBadge]);
|
}, [canSave, renderGlobalHelpControls, setBadge]);
|
||||||
|
|
||||||
kibanaService.core = core;
|
|
||||||
|
|
||||||
store.dispatch(setBasePath(basePath));
|
store.dispatch(setBasePath(basePath));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in a new issue