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