mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Alert search bar] Replace the status filter with controls on the observability pages (#198495)
Closes #197953 ## Summary This PR replaces the alert status filter with filter controls. In this PR, I also covered backward compatibility when we have a `status` URL parameter by passing that value to filters, as shown below: |State|Screenshot| |---|---| |Before|| |After|| https://github.com/user-attachments/assets/86e82a19-f68e-4127-9fd8-e0efe0d41ece I checked in Serverless and we have access to controls in viewer mode as well: https://github.com/user-attachments/assets/2c90ba3a-7d95-4682-b722-e5b327f7334d ### 🐞 Known issue 1. Privilege In Stateful, if a user has Kibana privilege but not the `.alert*` es privilege, then the controls do not work as expected. This issue will be tackled in a separate ticket: <details> <summary>This is the error that we show in this scenario</summary>  </details> 2. Initial load Related ticket: https://github.com/elastic/kibana/issues/183412 ### 🗒️ Tasks - [x] ~~Solving the permission issue~~ This issue does not happen in Serverless and for stateful, we will fix it in a separate ticket: https://github.com/elastic/kibana/issues/208225 - The main issue will be fixed in this [PR](https://github.com/elastic/kibana/pull/191110) - In the above [PR](https://github.com/elastic/kibana/pull/191110), we remove controls if the user does not have the privilege for alert indices, but we need to figure out how to adjust filter controls to access the data based on Kibana privileges. - [x] We should configure the filters to allow the selection of one item for alert status but still show the other options - [x] We need to see how we can make this work with the current status field. Ideally, if there is a status field, we would apply it and remove it from the URL. - Fixed in [c6cad2d
](c6cad2dbe1
) - [x] Changing the URL does not update the page filters correctly. ~~It might be related to https://github.com/elastic/kibana/issues/183412.~~ - [x] We need to make sure these adjustments work as expected in APM as they use the observability alert search bar. - [x] Check if the tags filter can be improved, and if not, whether it makes sense to keep it in its current form. - It works based on how array filtering works in ES, which seems like a good start to me. - [x] Check with Maciej: Do we need to disable changing control configs? - Checked with Maciej: it is fine to keep the option of editing controls. - [x] Do we need to have a different local storage item for each page (apm/rule details/alert details/alerts)? - How can we disable syncing with the local storage? - Added the possibility of disabling sync in [24bab21
](24bab210b0
) and disabled it for the rule details and alert details pages. - Also, disabled it for the APM alert search bar. - [x] Setting default status as active on the related alerts tab --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
parent
e3311c516b
commit
14b9a4828a
40 changed files with 900 additions and 314 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1387,6 +1387,7 @@ packages/kbn-monaco/src/esql @elastic/kibana-esql
|
|||
/x-pack/test/api_integration/deployment_agnostic/apis/observability/synthetics/ @elastic/obs-ux-management-team
|
||||
/x-pack/test/api_integration/deployment_agnostic/services/synthetics_monitors @elastic/obs-ux-management-team
|
||||
/x-pack/test/api_integration/deployment_agnostic/services/synthetics_private_location @elastic/obs-ux-management-team
|
||||
/x-pack/test/functional/page_objects/alert_controls.ts @elastic/obs-ux-management-team
|
||||
|
||||
# Elastic Stack Monitoring
|
||||
/x-pack/test/monitoring_api_integration @elastic/stack-monitoring
|
||||
|
|
|
@ -60,6 +60,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
Storage,
|
||||
ruleTypeIds,
|
||||
storageKey,
|
||||
disableLocalStorageSync = false,
|
||||
} = props;
|
||||
|
||||
const filterChangedSubscription = useRef<Subscription>();
|
||||
|
@ -105,7 +106,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
} = useControlGroupSyncToLocalStorage({
|
||||
Storage,
|
||||
storageKey: localStoragePageFilterKey,
|
||||
shouldSync: isViewMode,
|
||||
shouldSync: !disableLocalStorageSync && isViewMode,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -74,4 +74,5 @@ export interface FilterGroupProps extends Pick<ControlGroupRuntimeState, 'chaini
|
|||
ControlGroupRenderer: typeof ControlGroupRenderer;
|
||||
Storage: typeof Storage;
|
||||
storageKey?: string;
|
||||
disableLocalStorageSync?: boolean;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
export * from './use_alerts_data_view';
|
||||
export * from './use_fetch_alerts_index_names_query';
|
||||
export * from './use_get_alerts_group_aggregations_query';
|
||||
export * from './use_health_check';
|
||||
export * from './use_load_alerting_framework_health';
|
||||
|
|
|
@ -41,7 +41,7 @@ export const ENVIRONMENT_NOT_DEFINED = {
|
|||
label: getEnvironmentLabel(ENVIRONMENT_NOT_DEFINED_VALUE),
|
||||
};
|
||||
|
||||
function isEnvironmentDefined(environment: string) {
|
||||
export function isEnvironmentDefined(environment: string) {
|
||||
return (
|
||||
environment &&
|
||||
environment !== ENVIRONMENT_NOT_DEFINED_VALUE &&
|
||||
|
|
|
@ -36,14 +36,19 @@ describe('Alerts table', () => {
|
|||
});
|
||||
|
||||
it('Alerts table with the search bar is populated', () => {
|
||||
const expectedControls = ['Statusactive 1', 'Rule', 'Group', 'Tags'];
|
||||
|
||||
cy.visitKibana(serviceOverviewHref);
|
||||
cy.contains('opbeans-java');
|
||||
cy.get('[data-test-subj="environmentFilter"] [data-test-subj="comboBoxSearchInput"]').should(
|
||||
'have.value',
|
||||
'All'
|
||||
);
|
||||
cy.contains('Active');
|
||||
cy.contains('Recovered');
|
||||
cy.get('[data-test-subj="control-frame-title"]')
|
||||
.should('have.length', 4)
|
||||
.each(($el, index) => {
|
||||
cy.wrap($el)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
expect(text.trim()).to.equal(expectedControls[index]);
|
||||
});
|
||||
});
|
||||
cy.getByTestSubj('globalQueryBar').should('exist');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public';
|
||||
import type { AppMountParameters, CoreStart } from '@kbn/core/public';
|
||||
import { APP_WRAPPER_CLASS } from '@kbn/core/public';
|
||||
|
@ -62,6 +63,7 @@ export const renderApp = ({
|
|||
kibanaEnvironment,
|
||||
licensing: pluginsStart.licensing,
|
||||
};
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// render APM feedback link in global help menu
|
||||
setHelpExtension(coreStart);
|
||||
|
@ -82,11 +84,13 @@ export const renderApp = ({
|
|||
},
|
||||
}}
|
||||
>
|
||||
<ApmAppRoot
|
||||
apmPluginContextValue={apmPluginContextValue}
|
||||
pluginsStart={pluginsStart}
|
||||
apmServices={apmServices}
|
||||
/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApmAppRoot
|
||||
apmPluginContextValue={apmPluginContextValue}
|
||||
pluginsStart={pluginsStart}
|
||||
apmServices={apmServices}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</KibanaThemeProvider>
|
||||
</KibanaRenderContextProvider>,
|
||||
element
|
||||
|
|
|
@ -5,22 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ObservabilityAlertSearchBar } from '@kbn/observability-plugin/public';
|
||||
import type { AlertStatus } from '@kbn/observability-plugin/common/typings';
|
||||
import { EuiPanel, EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import type { BoolQuery, Filter } from '@kbn/es-query';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ObservabilityAlertsTable } from '@kbn/observability-plugin/public';
|
||||
import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import {
|
||||
APM_ALERTING_CONSUMERS,
|
||||
APM_ALERTING_RULE_TYPE_IDS,
|
||||
} from '../../../../common/alerting/config/apm_alerting_feature_ids';
|
||||
import type { ApmPluginStartDeps } from '../../../plugin';
|
||||
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
|
||||
import { SERVICE_NAME } from '../../../../common/es_fields/apm';
|
||||
import { getEnvironmentKuery } from '../../../../common/environment_filter_values';
|
||||
import { isEnvironmentDefined } from '../../../../common/environment_filter_values';
|
||||
import { SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../../common/es_fields/apm';
|
||||
import { push } from '../../shared/links/url_helpers';
|
||||
|
||||
export const ALERT_STATUS_ALL = 'all';
|
||||
|
@ -29,44 +29,53 @@ export function AlertsOverview() {
|
|||
const history = useHistory();
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { environment, rangeFrom, rangeTo, kuery, alertStatus },
|
||||
query: { environment, rangeFrom, rangeTo, kuery },
|
||||
} = useAnyOfApmParams('/services/{serviceName}/alerts', '/mobile-services/{serviceName}/alerts');
|
||||
const { services } = useKibana<ApmPluginStartDeps>();
|
||||
const [alertStatusFilter, setAlertStatusFilter] = useState<AlertStatus>(ALERT_STATUS_ALL);
|
||||
const {
|
||||
core: { http, notifications },
|
||||
} = useApmPluginContext();
|
||||
const [filterControls, setFilterControls] = useState<Filter[]>([]);
|
||||
const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>();
|
||||
|
||||
useEffect(() => {
|
||||
if (alertStatus) {
|
||||
setAlertStatusFilter(alertStatus as AlertStatus);
|
||||
}
|
||||
}, [alertStatus]);
|
||||
|
||||
const {
|
||||
triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar },
|
||||
notifications,
|
||||
data: {
|
||||
query: {
|
||||
timefilter: { timefilter: timeFilterService },
|
||||
},
|
||||
},
|
||||
data,
|
||||
dataViews,
|
||||
spaces,
|
||||
uiSettings,
|
||||
} = services;
|
||||
const {
|
||||
query: {
|
||||
timefilter: { timefilter: timeFilterService },
|
||||
},
|
||||
} = data;
|
||||
|
||||
const useToasts = () => notifications!.toasts;
|
||||
|
||||
const apmQueries = useMemo(() => {
|
||||
const environmentKuery = getEnvironmentKuery(environment);
|
||||
let query = `${SERVICE_NAME}:${serviceName}`;
|
||||
|
||||
if (environmentKuery) {
|
||||
query += ` AND ${environmentKuery}`;
|
||||
}
|
||||
return [
|
||||
const apmFilters = useMemo(() => {
|
||||
const filters: Filter[] = [
|
||||
{
|
||||
query,
|
||||
language: 'kuery',
|
||||
query: {
|
||||
match_phrase: {
|
||||
[SERVICE_NAME]: serviceName,
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
];
|
||||
|
||||
if (isEnvironmentDefined(environment)) {
|
||||
filters.push({
|
||||
query: {
|
||||
match_phrase: {
|
||||
[SERVICE_ENVIRONMENT]: environment,
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
});
|
||||
}
|
||||
return filters;
|
||||
}, [serviceName, environment]);
|
||||
|
||||
const onKueryChange = useCallback(
|
||||
|
@ -85,15 +94,21 @@ export function AlertsOverview() {
|
|||
onRangeFromChange={(value) => push(history, { query: { rangeFrom: value } })}
|
||||
onRangeToChange={(value) => push(history, { query: { rangeTo: value } })}
|
||||
onKueryChange={onKueryChange}
|
||||
defaultSearchQueries={apmQueries}
|
||||
onStatusChange={setAlertStatusFilter}
|
||||
defaultFilters={apmFilters}
|
||||
filterControls={filterControls}
|
||||
onFilterControlsChange={setFilterControls}
|
||||
onEsQueryChange={setEsQuery}
|
||||
rangeTo={rangeTo}
|
||||
rangeFrom={rangeFrom}
|
||||
status={alertStatusFilter}
|
||||
disableLocalStorageSync
|
||||
services={{
|
||||
timeFilterService,
|
||||
AlertsSearchBar,
|
||||
http,
|
||||
data,
|
||||
dataViews,
|
||||
notifications,
|
||||
spaces,
|
||||
useToasts,
|
||||
uiSettings,
|
||||
}}
|
||||
|
|
|
@ -6,19 +6,28 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { act, waitFor } from '@testing-library/react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { timefilterServiceMock } from '@kbn/data-plugin/public/query/timefilter/timefilter_service.mock';
|
||||
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
|
||||
|
||||
import { ObservabilityAlertSearchBarProps, Services } from './types';
|
||||
import { ObservabilityAlertSearchBar } from './alert_search_bar';
|
||||
import { render } from '../../utils/test_helper';
|
||||
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../../../common/constants';
|
||||
import { kibanaStartMock } from '../../utils/kibana_react.mock';
|
||||
import { render } from '../../utils/test_helper';
|
||||
import { ObservabilityAlertSearchBar } from './alert_search_bar';
|
||||
import { ObservabilityAlertSearchBarProps, Services } from './types';
|
||||
|
||||
const getAlertsSearchBarMock = jest.fn();
|
||||
const ALERT_SEARCH_BAR_DATA_TEST_SUBJ = 'alerts-search-bar';
|
||||
const ALERT_UUID = '413a9631-1a29-4344-a8b4-9a1dc23421ee';
|
||||
|
||||
describe('ObservabilityAlertSearchBar', () => {
|
||||
const { http, data, dataViews, notifications, spaces } = kibanaStartMock.startContract().services;
|
||||
spaces.getActiveSpace = jest
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ id: 'space-id', name: 'space-name', disabledFeatures: [] })
|
||||
);
|
||||
const renderComponent = (
|
||||
props: Partial<ObservabilityAlertSearchBarProps> = {},
|
||||
services: Partial<Services> = {}
|
||||
|
@ -27,12 +36,15 @@ describe('ObservabilityAlertSearchBar', () => {
|
|||
appName: 'testAppName',
|
||||
kuery: '',
|
||||
filters: [],
|
||||
filterControls: [],
|
||||
onRangeFromChange: jest.fn(),
|
||||
onRangeToChange: jest.fn(),
|
||||
onKueryChange: jest.fn(),
|
||||
onStatusChange: jest.fn(),
|
||||
onEsQueryChange: jest.fn(),
|
||||
onFiltersChange: jest.fn(),
|
||||
onControlConfigsChange: jest.fn(),
|
||||
onFilterControlsChange: jest.fn(),
|
||||
setSavedQuery: jest.fn(),
|
||||
rangeTo: 'now',
|
||||
rangeFrom: 'now-15m',
|
||||
|
@ -44,6 +56,11 @@ describe('ObservabilityAlertSearchBar', () => {
|
|||
<div data-test-subj={ALERT_SEARCH_BAR_DATA_TEST_SUBJ} />
|
||||
),
|
||||
useToasts: jest.fn(),
|
||||
http,
|
||||
data,
|
||||
dataViews,
|
||||
notifications,
|
||||
spaces,
|
||||
...services,
|
||||
},
|
||||
...props,
|
||||
|
@ -78,74 +95,34 @@ describe('ObservabilityAlertSearchBar', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should filter active alerts', async () => {
|
||||
it('should include defaultFilters in es query', async () => {
|
||||
const mockedOnEsQueryChange = jest.fn();
|
||||
const mockedFrom = '2022-11-15T09:38:13.604Z';
|
||||
const mockedTo = '2022-11-15T09:53:13.604Z';
|
||||
|
||||
renderComponent({
|
||||
onEsQueryChange: mockedOnEsQueryChange,
|
||||
rangeFrom: mockedFrom,
|
||||
rangeTo: mockedTo,
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
expect(mockedOnEsQueryChange).toHaveBeenCalledWith({
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [{ match_phrase: { 'kibana.alert.status': 'active' } }],
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'kibana.alert.time_range': expect.objectContaining({
|
||||
format: 'strict_date_optional_time',
|
||||
gte: mockedFrom,
|
||||
lte: mockedTo,
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should include defaultSearchQueries in es query', async () => {
|
||||
const mockedOnEsQueryChange = jest.fn();
|
||||
const mockedFrom = '2022-11-15T09:38:13.604Z';
|
||||
const mockedTo = '2022-11-15T09:53:13.604Z';
|
||||
const defaultSearchQueries = [
|
||||
const defaultFilters: Filter[] = [
|
||||
{
|
||||
query: 'kibana.alert.rule.uuid: 413a9631-1a29-4344-a8b4-9a1dc23421ee',
|
||||
language: 'kuery',
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.rule.uuid': ALERT_UUID,
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
];
|
||||
|
||||
renderComponent({
|
||||
onEsQueryChange: mockedOnEsQueryChange,
|
||||
rangeFrom: mockedFrom,
|
||||
rangeTo: mockedTo,
|
||||
defaultSearchQueries,
|
||||
status: 'all',
|
||||
await act(async () => {
|
||||
renderComponent({
|
||||
onEsQueryChange: mockedOnEsQueryChange,
|
||||
rangeFrom: mockedFrom,
|
||||
rangeTo: mockedTo,
|
||||
defaultFilters,
|
||||
status: 'all',
|
||||
});
|
||||
});
|
||||
|
||||
const esQueryChangeParams = {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{ match: { 'kibana.alert.rule.uuid': '413a9631-1a29-4344-a8b4-9a1dc23421ee' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'kibana.alert.time_range': expect.objectContaining({
|
||||
|
@ -155,15 +132,71 @@ describe('ObservabilityAlertSearchBar', () => {
|
|||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
match_phrase: {
|
||||
'kibana.alert.rule.uuid': ALERT_UUID,
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
};
|
||||
expect(mockedOnEsQueryChange).toHaveBeenCalledTimes(2);
|
||||
expect(mockedOnEsQueryChange).toHaveBeenCalledTimes(1);
|
||||
expect(mockedOnEsQueryChange).toHaveBeenNthCalledWith(1, esQueryChangeParams);
|
||||
});
|
||||
|
||||
it('should include filterControls in es query', async () => {
|
||||
const mockedOnEsQueryChange = jest.fn();
|
||||
const mockedFrom = '2022-11-15T09:38:13.604Z';
|
||||
const mockedTo = '2022-11-15T09:53:13.604Z';
|
||||
const filterControls: Filter[] = [
|
||||
{
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.rule.uuid': ALERT_UUID,
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
];
|
||||
|
||||
await act(async () => {
|
||||
renderComponent({
|
||||
onEsQueryChange: mockedOnEsQueryChange,
|
||||
rangeFrom: mockedFrom,
|
||||
rangeTo: mockedTo,
|
||||
filterControls,
|
||||
status: 'all',
|
||||
});
|
||||
});
|
||||
|
||||
const esQueryChangeParams = {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'kibana.alert.time_range': expect.objectContaining({
|
||||
format: 'strict_date_optional_time',
|
||||
gte: mockedFrom,
|
||||
lte: mockedTo,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
match_phrase: {
|
||||
'kibana.alert.rule.uuid': ALERT_UUID,
|
||||
},
|
||||
},
|
||||
],
|
||||
must: [],
|
||||
must_not: [],
|
||||
should: [],
|
||||
},
|
||||
};
|
||||
expect(mockedOnEsQueryChange).toHaveBeenCalledTimes(1);
|
||||
expect(mockedOnEsQueryChange).toHaveBeenNthCalledWith(1, esQueryChangeParams);
|
||||
expect(mockedOnEsQueryChange).toHaveBeenNthCalledWith(2, esQueryChangeParams);
|
||||
});
|
||||
|
||||
it('should include filters in es query', async () => {
|
||||
|
|
|
@ -5,90 +5,73 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { AlertFilterControls } from '@kbn/alerts-ui-shared/src/alert_filter_controls';
|
||||
import { useFetchAlertsIndexNamesQuery } from '@kbn/alerts-ui-shared';
|
||||
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../../../common/constants';
|
||||
import { AlertsStatusFilter } from './components';
|
||||
import { ALERT_STATUS_QUERY, DEFAULT_QUERIES, DEFAULT_QUERY_STRING } from './constants';
|
||||
import { DEFAULT_QUERY_STRING, EMPTY_FILTERS } from './constants';
|
||||
import { ObservabilityAlertSearchBarProps } from './types';
|
||||
import { buildEsQuery } from '../../utils/build_es_query';
|
||||
import { AlertStatus } from '../../../common/typings';
|
||||
|
||||
const getAlertStatusQuery = (status: string): Query[] => {
|
||||
return ALERT_STATUS_QUERY[status]
|
||||
? [{ query: ALERT_STATUS_QUERY[status], language: 'kuery' }]
|
||||
: [];
|
||||
};
|
||||
const toastTitle = i18n.translate('xpack.observability.alerts.searchBar.invalidQueryTitle', {
|
||||
defaultMessage: 'Invalid query string',
|
||||
});
|
||||
const defaultFilters: Filter[] = [];
|
||||
|
||||
export function ObservabilityAlertSearchBar({
|
||||
appName,
|
||||
defaultSearchQueries = DEFAULT_QUERIES,
|
||||
defaultFilters = EMPTY_FILTERS,
|
||||
disableLocalStorageSync,
|
||||
onEsQueryChange,
|
||||
onKueryChange,
|
||||
onRangeFromChange,
|
||||
onRangeToChange,
|
||||
onStatusChange,
|
||||
onControlConfigsChange,
|
||||
onFiltersChange,
|
||||
onFilterControlsChange,
|
||||
showFilterBar = false,
|
||||
filters = defaultFilters,
|
||||
controlConfigs,
|
||||
filters = EMPTY_FILTERS,
|
||||
filterControls = EMPTY_FILTERS,
|
||||
savedQuery,
|
||||
setSavedQuery,
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
services: { AlertsSearchBar, timeFilterService, useToasts, uiSettings },
|
||||
status,
|
||||
onControlApiAvailable,
|
||||
services: {
|
||||
AlertsSearchBar,
|
||||
timeFilterService,
|
||||
http,
|
||||
notifications,
|
||||
dataViews,
|
||||
spaces,
|
||||
useToasts,
|
||||
uiSettings,
|
||||
},
|
||||
}: ObservabilityAlertSearchBarProps) {
|
||||
const toasts = useToasts();
|
||||
const [spaceId, setSpaceId] = useState<string>();
|
||||
const queryFilter = kuery ? { query: kuery, language: 'kuery' } : undefined;
|
||||
const { data: indexNames } = useFetchAlertsIndexNamesQuery({
|
||||
http,
|
||||
ruleTypeIds: OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
|
||||
});
|
||||
|
||||
const clearSavedQuery = useCallback(
|
||||
() => (setSavedQuery ? setSavedQuery(undefined) : null),
|
||||
[setSavedQuery]
|
||||
);
|
||||
const onAlertStatusChange = useCallback(
|
||||
(alertStatus: AlertStatus) => {
|
||||
try {
|
||||
onEsQueryChange(
|
||||
buildEsQuery({
|
||||
timeRange: {
|
||||
to: rangeTo,
|
||||
from: rangeFrom,
|
||||
},
|
||||
kuery,
|
||||
queries: [...getAlertStatusQuery(alertStatus), ...defaultSearchQueries],
|
||||
config: getEsQueryConfig(uiSettings),
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
toasts.addError(error, {
|
||||
title: toastTitle,
|
||||
});
|
||||
onKueryChange(DEFAULT_QUERY_STRING);
|
||||
}
|
||||
},
|
||||
[
|
||||
onEsQueryChange,
|
||||
rangeTo,
|
||||
rangeFrom,
|
||||
kuery,
|
||||
defaultSearchQueries,
|
||||
uiSettings,
|
||||
toasts,
|
||||
onKueryChange,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onAlertStatusChange(status);
|
||||
}, [onAlertStatusChange, status]);
|
||||
const filterControlsStorageKey = useMemo(
|
||||
() => ['observabilitySearchBar', spaceId, appName, 'filterControls'].filter(Boolean).join('.'),
|
||||
[appName, spaceId]
|
||||
);
|
||||
|
||||
const submitQuery = useCallback(() => {
|
||||
try {
|
||||
|
@ -99,8 +82,7 @@ export function ObservabilityAlertSearchBar({
|
|||
from: rangeFrom,
|
||||
},
|
||||
kuery,
|
||||
queries: [...getAlertStatusQuery(status), ...defaultSearchQueries],
|
||||
filters,
|
||||
filters: [...filters, ...filterControls, ...defaultFilters],
|
||||
config: getEsQueryConfig(uiSettings),
|
||||
})
|
||||
);
|
||||
|
@ -111,22 +93,28 @@ export function ObservabilityAlertSearchBar({
|
|||
onKueryChange(DEFAULT_QUERY_STRING);
|
||||
}
|
||||
}, [
|
||||
defaultSearchQueries,
|
||||
filters,
|
||||
kuery,
|
||||
onEsQueryChange,
|
||||
onKueryChange,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
status,
|
||||
rangeFrom,
|
||||
kuery,
|
||||
defaultFilters,
|
||||
filters,
|
||||
filterControls,
|
||||
uiSettings,
|
||||
toasts,
|
||||
onKueryChange,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
submitQuery();
|
||||
}, [submitQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (spaces) {
|
||||
spaces.getActiveSpace().then((space) => setSpaceId(space.id));
|
||||
}
|
||||
}, [spaces]);
|
||||
|
||||
const onQuerySubmit = (
|
||||
{
|
||||
dateRange,
|
||||
|
@ -176,11 +164,31 @@ export function ObservabilityAlertSearchBar({
|
|||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AlertsStatusFilter status={status} onChange={onStatusChange} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{indexNames && indexNames.length > 0 && (
|
||||
<AlertFilterControls
|
||||
dataViewSpec={{
|
||||
id: 'observability-unified-alerts-dv',
|
||||
title: indexNames.join(','),
|
||||
}}
|
||||
spaceId={spaceId}
|
||||
chainingSystem="HIERARCHICAL"
|
||||
controlsUrlState={controlConfigs}
|
||||
setControlsUrlState={onControlConfigsChange}
|
||||
filters={[...filters, ...defaultFilters]}
|
||||
onFiltersChange={onFilterControlsChange}
|
||||
storageKey={filterControlsStorageKey}
|
||||
disableLocalStorageSync={disableLocalStorageSync}
|
||||
query={queryFilter}
|
||||
services={{
|
||||
http,
|
||||
notifications,
|
||||
dataViews,
|
||||
storage: Storage,
|
||||
}}
|
||||
ControlGroupRenderer={ControlGroupRenderer}
|
||||
onInit={onControlApiAvailable}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
alertSearchBarStateContainer,
|
||||
Provider,
|
||||
|
@ -20,22 +21,40 @@ import { useToasts } from '../../hooks/use_toast';
|
|||
function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
|
||||
const { urlStorageKey, defaultState = DEFAULT_STATE, ...searchBarProps } = props;
|
||||
const stateProps = useAlertSearchBarStateContainer(urlStorageKey, undefined, defaultState);
|
||||
const [filterControls, setFilterControls] = useState<Filter[]>([]);
|
||||
const {
|
||||
data: {
|
||||
query: {
|
||||
timefilter: { timefilter: timeFilterService },
|
||||
},
|
||||
},
|
||||
data,
|
||||
triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar },
|
||||
uiSettings,
|
||||
http,
|
||||
dataViews,
|
||||
spaces,
|
||||
notifications,
|
||||
} = useKibana().services;
|
||||
const {
|
||||
query: {
|
||||
timefilter: { timefilter: timeFilterService },
|
||||
},
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<ObservabilityAlertSearchBar
|
||||
{...stateProps}
|
||||
{...searchBarProps}
|
||||
filterControls={filterControls}
|
||||
onFilterControlsChange={setFilterControls}
|
||||
showFilterBar
|
||||
services={{ timeFilterService, AlertsSearchBar, useToasts, uiSettings }}
|
||||
services={{
|
||||
timeFilterService,
|
||||
AlertsSearchBar,
|
||||
http,
|
||||
data,
|
||||
dataViews,
|
||||
notifications,
|
||||
spaces,
|
||||
useToasts,
|
||||
uiSettings,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
ALERT_STATUS_ACTIVE,
|
||||
|
@ -16,7 +16,7 @@ import {
|
|||
import { AlertStatusFilter } from '../../../common/typings';
|
||||
import { ALERT_STATUS_ALL } from '../../../common/constants';
|
||||
|
||||
export const DEFAULT_QUERIES: Query[] = [];
|
||||
export const EMPTY_FILTERS: Filter[] = [];
|
||||
export const DEFAULT_QUERY_STRING = '';
|
||||
|
||||
export const ALL_ALERTS: AlertStatusFilter = {
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { type FilterControlConfig } from '@kbn/alerts-ui-shared';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import {
|
||||
createStateContainer,
|
||||
createStateContainerReactHelpers,
|
||||
} from '@kbn/kibana-utils-plugin/public';
|
||||
import { AlertStatus } from '../../../../common/typings';
|
||||
import { ALL_ALERTS } from '../constants';
|
||||
import { AlertSearchBarContainerState } from '../types';
|
||||
|
||||
interface AlertSearchBarStateTransitions {
|
||||
|
@ -33,13 +33,15 @@ interface AlertSearchBarStateTransitions {
|
|||
setSavedQueryId: (
|
||||
state: AlertSearchBarContainerState
|
||||
) => (savedQueryId?: string) => AlertSearchBarContainerState;
|
||||
setControlConfigs: (
|
||||
state: AlertSearchBarContainerState
|
||||
) => (controlConfigs: FilterControlConfig[]) => AlertSearchBarContainerState;
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: AlertSearchBarContainerState = {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
kuery: '',
|
||||
status: ALL_ALERTS.status,
|
||||
filters: [],
|
||||
};
|
||||
|
||||
|
@ -50,6 +52,7 @@ const transitions: AlertSearchBarStateTransitions = {
|
|||
setStatus: (state) => (status) => ({ ...state, status }),
|
||||
setFilters: (state) => (filters) => ({ ...state, filters }),
|
||||
setSavedQueryId: (state) => (savedQueryId) => ({ ...state, savedQueryId }),
|
||||
setControlConfigs: (state) => (controlConfigs) => ({ ...state, controlConfigs }),
|
||||
};
|
||||
|
||||
const alertSearchBarStateContainer = createStateContainer(DEFAULT_STATE, transitions);
|
||||
|
|
|
@ -10,7 +10,12 @@ import { pipe } from 'fp-ts/pipeable';
|
|||
import * as t from 'io-ts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
|
||||
import { DEFAULT_CONTROLS } from '@kbn/alerts-ui-shared/src/alert_filter_controls/constants';
|
||||
import {
|
||||
ALERT_STATUS_ACTIVE,
|
||||
ALERT_STATUS_RECOVERED,
|
||||
ALERT_STATUS_UNTRACKED,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { SavedQuery, TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
createKbnUrlStateStorage,
|
||||
|
@ -18,6 +23,7 @@ import {
|
|||
IKbnUrlStateStorage,
|
||||
useContainerSelector,
|
||||
} from '@kbn/kibana-utils-plugin/public';
|
||||
import { setStatusOnControlConfigs } from '../../../utils/alert_controls/set_status_on_control_configs';
|
||||
import { datemathStringRT } from '../../../utils/datemath';
|
||||
import { ALERT_STATUS_ALL } from '../../../../common/constants';
|
||||
import { useTimefilterService } from '../../../hooks/use_timefilter_service';
|
||||
|
@ -37,6 +43,7 @@ export const alertSearchBarState = t.partial({
|
|||
t.literal(ALERT_STATUS_ACTIVE),
|
||||
t.literal(ALERT_STATUS_RECOVERED),
|
||||
t.literal(ALERT_STATUS_ALL),
|
||||
t.literal(ALERT_STATUS_UNTRACKED),
|
||||
]),
|
||||
});
|
||||
|
||||
|
@ -50,12 +57,17 @@ export function useAlertSearchBarStateContainer(
|
|||
|
||||
useUrlStateSyncEffect(stateContainer, urlStorageKey, replace, defaultState);
|
||||
|
||||
const { setRangeFrom, setRangeTo, setKuery, setStatus, setFilters, setSavedQueryId } =
|
||||
stateContainer.transitions;
|
||||
const { rangeFrom, rangeTo, kuery, status, filters, savedQueryId } = useContainerSelector(
|
||||
stateContainer,
|
||||
(state) => state
|
||||
);
|
||||
const {
|
||||
setRangeFrom,
|
||||
setRangeTo,
|
||||
setKuery,
|
||||
setStatus,
|
||||
setFilters,
|
||||
setSavedQueryId,
|
||||
setControlConfigs,
|
||||
} = stateContainer.transitions;
|
||||
const { rangeFrom, rangeTo, kuery, status, filters, savedQueryId, controlConfigs } =
|
||||
useContainerSelector(stateContainer, (state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
if (!savedQuery) {
|
||||
|
@ -94,6 +106,8 @@ export function useAlertSearchBarStateContainer(
|
|||
onRangeToChange: setRangeTo,
|
||||
onStatusChange: setStatus,
|
||||
onFiltersChange: setFilters,
|
||||
onControlConfigsChange: setControlConfigs,
|
||||
controlConfigs,
|
||||
filters,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
|
@ -176,7 +190,9 @@ function initializeUrlAndStateContainer(
|
|||
const urlState = alertSearchBarState.decode(
|
||||
urlStateStorage.get<Partial<AlertSearchBarContainerState>>(urlStorageKey)
|
||||
);
|
||||
const validUrlState = isRight(urlState) ? pipe(urlState).right : {};
|
||||
const validUrlState: Partial<AlertSearchBarContainerState> = isRight(urlState)
|
||||
? pipe(urlState).right
|
||||
: {};
|
||||
const timeFilterTime = timefilterService.getTime();
|
||||
const timeFilterState = timefilterService.isTimeTouched()
|
||||
? {
|
||||
|
@ -185,6 +201,16 @@ function initializeUrlAndStateContainer(
|
|||
}
|
||||
: {};
|
||||
|
||||
// This part is for backward compatibility. Previously, we saved status in the status query
|
||||
// parameter. Now, we save it in the controlConfigs.
|
||||
if (validUrlState.status) {
|
||||
validUrlState.controlConfigs = setStatusOnControlConfigs(
|
||||
validUrlState.status,
|
||||
validUrlState.controlConfigs ?? DEFAULT_CONTROLS
|
||||
);
|
||||
validUrlState.status = undefined;
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...defaultState,
|
||||
...timeFilterState,
|
||||
|
|
|
@ -6,10 +6,18 @@
|
|||
*/
|
||||
|
||||
import { ReactElement } from 'react';
|
||||
import { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import { type SavedQuery, TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import { type FilterControlConfig, FilterGroupHandler } from '@kbn/alerts-ui-shared';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import { type NotificationsStart, ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import {
|
||||
DataPublicPluginStart,
|
||||
type SavedQuery,
|
||||
TimefilterContract,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { AlertsSearchBarProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_search_bar';
|
||||
import { BoolQuery, Filter, Query } from '@kbn/es-query';
|
||||
import { BoolQuery, Filter } from '@kbn/es-query';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { AlertStatus } from '../../../common/typings';
|
||||
export interface AlertStatusFilterProps {
|
||||
|
@ -20,6 +28,7 @@ export interface AlertStatusFilterProps {
|
|||
export interface AlertSearchBarWithUrlSyncProps extends CommonAlertSearchBarProps {
|
||||
urlStorageKey: string;
|
||||
defaultState?: AlertSearchBarContainerState;
|
||||
disableLocalStorageSync?: boolean;
|
||||
}
|
||||
|
||||
export interface Dependencies {
|
||||
|
@ -37,6 +46,11 @@ export interface Dependencies {
|
|||
export interface Services {
|
||||
timeFilterService: TimefilterContract;
|
||||
AlertsSearchBar: (props: AlertsSearchBarProps) => ReactElement<AlertsSearchBarProps>;
|
||||
http: HttpStart;
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
notifications: NotificationsStart;
|
||||
spaces?: SpacesPluginStart;
|
||||
useToasts: () => ToastsStart;
|
||||
uiSettings: IUiSettingsClient;
|
||||
}
|
||||
|
@ -45,31 +59,37 @@ export interface ObservabilityAlertSearchBarProps
|
|||
extends AlertSearchBarContainerState,
|
||||
AlertSearchBarStateTransitions,
|
||||
CommonAlertSearchBarProps {
|
||||
showFilterBar?: boolean;
|
||||
savedQuery?: SavedQuery;
|
||||
services: Services;
|
||||
filterControls: Filter[];
|
||||
onFilterControlsChange: (controlConfigs: Filter[]) => void;
|
||||
savedQuery?: SavedQuery;
|
||||
showFilterBar?: boolean;
|
||||
disableLocalStorageSync?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertSearchBarContainerState {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
kuery: string;
|
||||
status: AlertStatus;
|
||||
status?: AlertStatus;
|
||||
filters?: Filter[];
|
||||
savedQueryId?: string;
|
||||
controlConfigs?: FilterControlConfig[];
|
||||
}
|
||||
|
||||
interface AlertSearchBarStateTransitions {
|
||||
onRangeFromChange: (rangeFrom: string) => void;
|
||||
onRangeToChange: (rangeTo: string) => void;
|
||||
onKueryChange: (kuery: string) => void;
|
||||
onStatusChange: (status: AlertStatus) => void;
|
||||
onStatusChange?: (status: AlertStatus) => void;
|
||||
onFiltersChange?: (filters: Filter[]) => void;
|
||||
setSavedQuery?: (savedQueryId?: SavedQuery) => void;
|
||||
onControlConfigsChange?: (controlConfigs: FilterControlConfig[]) => void;
|
||||
}
|
||||
|
||||
interface CommonAlertSearchBarProps {
|
||||
appName: string;
|
||||
onEsQueryChange: (query: { bool: BoolQuery }) => void;
|
||||
defaultSearchQueries?: Query[];
|
||||
defaultFilters?: Filter[];
|
||||
onControlApiAvailable?: (controlGroupHandler: FilterGroupHandler | undefined) => void;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ACTIVE_ALERTS } from '../components/alert_search_bar/constants';
|
||||
import { ALERT_RULE_NAME, ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
RULE_DETAILS_EXECUTION_TAB,
|
||||
RULE_DETAILS_ALERTS_TAB,
|
||||
|
@ -37,7 +37,10 @@ describe('RuleDetailsLocator', () => {
|
|||
tabId: RULE_DETAILS_ALERTS_TAB,
|
||||
});
|
||||
expect(location.path).toEqual(
|
||||
`${RULES_PATH}/${mockedRuleId}?tabId=alerts&searchBarParams=(kuery:'',rangeFrom:now-15m,rangeTo:now,status:all)`
|
||||
`${RULES_PATH}/${mockedRuleId}?tabId=alerts&searchBarParams=(` +
|
||||
`controlConfigs:!((fieldName:kibana.alert.status,hideActionBar:!t,hideExists:!t,persist:!t,selectedOptions:!(active)` +
|
||||
`,title:Status),(fieldName:kibana.alert.rule.name,hideExists:!t,title:Rule),(fieldName:kibana.alert.group.value,title:Group)` +
|
||||
`,(fieldName:tags,title:Tags)),kuery:'',rangeFrom:now-15m,rangeTo:now)`
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -48,10 +51,52 @@ describe('RuleDetailsLocator', () => {
|
|||
rangeFrom: 'mockedRangeTo',
|
||||
rangeTo: 'mockedRangeFrom',
|
||||
kuery: 'mockedKuery',
|
||||
status: ACTIVE_ALERTS.status,
|
||||
});
|
||||
expect(location.path).toEqual(
|
||||
`${RULES_PATH}/${mockedRuleId}?tabId=alerts&searchBarParams=(kuery:mockedKuery,rangeFrom:mockedRangeTo,rangeTo:mockedRangeFrom,status:active)`
|
||||
`${RULES_PATH}/${mockedRuleId}?tabId=alerts&searchBarParams=(` +
|
||||
`controlConfigs:!((fieldName:kibana.alert.status,hideActionBar:!t,hideExists:!t,persist:!t,selectedOptions:!(active)` +
|
||||
`,title:Status),(fieldName:kibana.alert.rule.name,hideExists:!t,title:Rule),(fieldName:kibana.alert.group.value,title:Group)` +
|
||||
`,(fieldName:tags,title:Tags)),kuery:mockedKuery,rangeFrom:mockedRangeTo,rangeTo:mockedRangeFrom)`
|
||||
);
|
||||
});
|
||||
|
||||
it('should return correct url when controlConfigs is provided', async () => {
|
||||
const mockedControlConfigs = [
|
||||
{
|
||||
title: 'Status',
|
||||
fieldName: ALERT_STATUS,
|
||||
selectedOptions: ['untracked'],
|
||||
hideActionBar: true,
|
||||
persist: true,
|
||||
hideExists: true,
|
||||
},
|
||||
{
|
||||
title: 'Rule',
|
||||
fieldName: ALERT_RULE_NAME,
|
||||
hideExists: true,
|
||||
},
|
||||
{
|
||||
title: 'Group',
|
||||
fieldName: 'kibana.alert.group.value',
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
fieldName: 'tags',
|
||||
},
|
||||
];
|
||||
const location = await locator.getLocation({
|
||||
ruleId: mockedRuleId,
|
||||
tabId: RULE_DETAILS_ALERTS_TAB,
|
||||
rangeFrom: 'mockedRangeTo',
|
||||
rangeTo: 'mockedRangeFrom',
|
||||
kuery: 'mockedKuery',
|
||||
controlConfigs: mockedControlConfigs,
|
||||
});
|
||||
expect(location.path).toEqual(
|
||||
`${RULES_PATH}/${mockedRuleId}?tabId=alerts&searchBarParams=(` +
|
||||
`controlConfigs:!((fieldName:kibana.alert.status,hideActionBar:!t,hideExists:!t,persist:!t,selectedOptions:!(untracked)` +
|
||||
`,title:Status),(fieldName:kibana.alert.rule.name,hideExists:!t,title:Rule),(fieldName:kibana.alert.group.value,title:Group)` +
|
||||
`,(fieldName:tags,title:Tags)),kuery:mockedKuery,rangeFrom:mockedRangeTo,rangeTo:mockedRangeFrom)`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,27 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FilterControlConfig } from '@kbn/alerts-ui-shared';
|
||||
import { DEFAULT_CONTROLS } from '@kbn/alerts-ui-shared/src/alert_filter_controls/constants';
|
||||
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { LocatorDefinition } from '@kbn/share-plugin/public';
|
||||
import { ruleDetailsLocatorID } from '../../common';
|
||||
import { RULES_PATH } from '../../common/locators/paths';
|
||||
import { ALL_ALERTS } from '../components/alert_search_bar/constants';
|
||||
import {
|
||||
RULE_DETAILS_ALERTS_TAB,
|
||||
RULE_DETAILS_EXECUTION_TAB,
|
||||
RULE_DETAILS_SEARCH_BAR_URL_STORAGE_KEY,
|
||||
} from '../pages/rule_details/constants';
|
||||
import type { TabId } from '../pages/rule_details/rule_details';
|
||||
import type { AlertStatus } from '../../common/typings';
|
||||
|
||||
type RuleDetailsControlConfigs = Array<Omit<FilterControlConfig, 'sort'>>;
|
||||
export interface RuleDetailsLocatorParams extends SerializableRecord {
|
||||
ruleId: string;
|
||||
tabId?: TabId;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
kuery?: string;
|
||||
status?: AlertStatus;
|
||||
controlConfigs?: RuleDetailsControlConfigs;
|
||||
}
|
||||
|
||||
export const getRuleDetailsPath = (ruleId: string) => {
|
||||
|
@ -36,19 +37,19 @@ export class RuleDetailsLocatorDefinition implements LocatorDefinition<RuleDetai
|
|||
public readonly id = ruleDetailsLocatorID;
|
||||
|
||||
public readonly getLocation = async (params: RuleDetailsLocatorParams) => {
|
||||
const { ruleId, kuery, rangeTo, tabId, rangeFrom, status } = params;
|
||||
const { controlConfigs, ruleId, kuery, rangeTo, tabId, rangeFrom } = params;
|
||||
const appState: {
|
||||
tabId?: TabId;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
kuery?: string;
|
||||
status?: AlertStatus;
|
||||
controlConfigs?: RuleDetailsControlConfigs;
|
||||
} = {};
|
||||
|
||||
appState.rangeFrom = rangeFrom || 'now-15m';
|
||||
appState.rangeTo = rangeTo || 'now';
|
||||
appState.kuery = kuery || '';
|
||||
appState.status = status || ALL_ALERTS.status;
|
||||
appState.controlConfigs = controlConfigs ?? DEFAULT_CONTROLS;
|
||||
|
||||
let path = getRuleDetailsPath(ruleId);
|
||||
|
||||
|
|
|
@ -33,9 +33,15 @@ jest.mock('@kbn/alerts-grouping', () => ({
|
|||
|
||||
const useKibanaMock = useKibana as jest.Mock;
|
||||
const mockKibana = () => {
|
||||
const services = kibanaStartMock.startContract().services;
|
||||
services.spaces.getActiveSpace = jest
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ id: 'space-id', name: 'space-name', disabledFeatures: [] })
|
||||
);
|
||||
useKibanaMock.mockReturnValue({
|
||||
services: {
|
||||
...kibanaStartMock.startContract().services,
|
||||
...services,
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: jest.fn(),
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
ALERT_UUID,
|
||||
TAGS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { BoolQuery, Filter, type Query } from '@kbn/es-query';
|
||||
import { BoolQuery, Filter } from '@kbn/es-query';
|
||||
import { AlertsGrouping } from '@kbn/alerts-grouping';
|
||||
import { GroupingToolbarControls } from '../../../components/alerts_table/grouping/grouping_toolbar_controls';
|
||||
import { ObservabilityFields } from '../../../../common/utils/alerting/types';
|
||||
|
@ -48,7 +48,7 @@ import { renderGroupPanel } from '../../../components/alerts_table/grouping/rend
|
|||
import { getGroupStats } from '../../../components/alerts_table/grouping/get_group_stats';
|
||||
import { getAggregationsByGroupingField } from '../../../components/alerts_table/grouping/get_aggregations_by_grouping_field';
|
||||
import { DEFAULT_GROUPING_OPTIONS } from '../../../components/alerts_table/grouping/constants';
|
||||
import { ALERT_STATUS_FILTER } from '../../../components/alert_search_bar/constants';
|
||||
import { ACTIVE_ALERTS, ALERT_STATUS_FILTER } from '../../../components/alert_search_bar/constants';
|
||||
import { AlertsByGroupingAgg } from '../../../components/alerts_table/types';
|
||||
import {
|
||||
alertSearchBarStateContainer,
|
||||
|
@ -69,7 +69,8 @@ interface Props {
|
|||
alert?: TopAlert<ObservabilityFields>;
|
||||
}
|
||||
|
||||
const defaultState: AlertSearchBarContainerState = { ...DEFAULT_STATE, status: 'active' };
|
||||
// TODO: Bring back setting default status filter as active
|
||||
const defaultState: AlertSearchBarContainerState = { ...DEFAULT_STATE };
|
||||
const DEFAULT_FILTERS: Filter[] = [];
|
||||
|
||||
export function InternalRelatedAlerts({ alert }: Props) {
|
||||
|
@ -89,8 +90,17 @@ export function InternalRelatedAlerts({ alert }: Props) {
|
|||
const sharedFields = getSharedFields(alert?.fields);
|
||||
const kuery = getRelatedAlertKuery({ tags, groups, ruleId, sharedFields });
|
||||
|
||||
const defaultQuery = useRef<Query[]>([
|
||||
{ query: `not kibana.alert.uuid: ${alertId}`, language: 'kuery' },
|
||||
const defaultFilters = useRef<Filter[]>([
|
||||
{
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.uuid': alertId,
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
negate: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -112,7 +122,8 @@ export function InternalRelatedAlerts({ alert }: Props) {
|
|||
appName={RELATED_ALERTS_SEARCH_BAR_ID}
|
||||
onEsQueryChange={setEsQuery}
|
||||
urlStorageKey={SEARCH_BAR_URL_STORAGE_KEY}
|
||||
defaultSearchQueries={defaultQuery.current}
|
||||
defaultFilters={defaultFilters.current}
|
||||
disableLocalStorageSync={true}
|
||||
defaultState={{
|
||||
...defaultState,
|
||||
kuery,
|
||||
|
@ -124,7 +135,10 @@ export function InternalRelatedAlerts({ alert }: Props) {
|
|||
<AlertsGrouping<AlertsByGroupingAgg>
|
||||
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
|
||||
consumers={observabilityAlertFeatureIds}
|
||||
defaultFilters={ALERT_STATUS_FILTER[alertSearchBarStateProps.status] ?? DEFAULT_FILTERS}
|
||||
defaultFilters={
|
||||
ALERT_STATUS_FILTER[alertSearchBarStateProps.status ?? ACTIVE_ALERTS.status] ??
|
||||
DEFAULT_FILTERS
|
||||
}
|
||||
from={alertSearchBarStateProps.rangeFrom}
|
||||
to={alertSearchBarStateProps.rangeTo}
|
||||
globalFilters={alertSearchBarStateProps.filters ?? DEFAULT_FILTERS}
|
||||
|
|
|
@ -40,6 +40,11 @@ mockUseKibanaReturnValue.services.application.capabilities = {
|
|||
show: true,
|
||||
},
|
||||
};
|
||||
mockUseKibanaReturnValue.services.spaces.getActiveSpace = jest
|
||||
.fn()
|
||||
.mockImplementation(() =>
|
||||
Promise.resolve({ id: 'space-id', name: 'space-name', disabledFeatures: [] })
|
||||
);
|
||||
|
||||
const mockObservabilityAIAssistant = observabilityAIAssistantPluginMock.createStartContract();
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { BrushEndListener, XYBrushEvent } from '@elastic/charts';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { BoolQuery, Filter } from '@kbn/es-query';
|
||||
import { usePerformanceContext } from '@kbn/ebt-tools';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -18,7 +18,6 @@ import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
|||
import { AlertsGrouping } from '@kbn/alerts-grouping';
|
||||
|
||||
import { rulesLocatorID } from '../../../common';
|
||||
import { ALERT_STATUS_FILTER } from '../../components/alert_search_bar/constants';
|
||||
import { renderGroupPanel } from '../../components/alerts_table/grouping/render_group_panel';
|
||||
import { getGroupStats } from '../../components/alerts_table/grouping/get_group_stats';
|
||||
import { getAggregationsByGroupingField } from '../../components/alerts_table/grouping/get_aggregations_by_grouping_field';
|
||||
|
@ -78,6 +77,7 @@ function InternalAlertsPage() {
|
|||
share: {
|
||||
url: { locators },
|
||||
},
|
||||
spaces,
|
||||
triggersActionsUi: {
|
||||
getAlertsSearchBar: AlertsSearchBar,
|
||||
getAlertSummaryWidget: AlertSummaryWidget,
|
||||
|
@ -92,6 +92,7 @@ function InternalAlertsPage() {
|
|||
},
|
||||
} = data;
|
||||
const { ObservabilityPageTemplate } = usePluginContext();
|
||||
const [filterControls, setFilterControls] = useState<Filter[]>([]);
|
||||
const alertSearchBarStateProps = useAlertSearchBarStateContainer(ALERTS_URL_STORAGE_KEY, {
|
||||
replace: false,
|
||||
});
|
||||
|
@ -267,9 +268,22 @@ function InternalAlertsPage() {
|
|||
{...alertSearchBarStateProps}
|
||||
appName={ALERTS_SEARCH_BAR_ID}
|
||||
onEsQueryChange={setEsQuery}
|
||||
filterControls={filterControls}
|
||||
onFilterControlsChange={setFilterControls}
|
||||
showFilterBar
|
||||
services={{ timeFilterService, AlertsSearchBar, useToasts, uiSettings }}
|
||||
services={{
|
||||
timeFilterService,
|
||||
AlertsSearchBar,
|
||||
http,
|
||||
data,
|
||||
dataViews,
|
||||
notifications,
|
||||
spaces,
|
||||
useToasts,
|
||||
uiSettings,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<AlertSummaryWidget
|
||||
|
@ -289,12 +303,12 @@ function InternalAlertsPage() {
|
|||
<AlertsGrouping<AlertsByGroupingAgg>
|
||||
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
|
||||
consumers={observabilityAlertFeatureIds}
|
||||
defaultFilters={
|
||||
ALERT_STATUS_FILTER[alertSearchBarStateProps.status] ?? DEFAULT_FILTERS
|
||||
}
|
||||
from={alertSearchBarStateProps.rangeFrom}
|
||||
to={alertSearchBarStateProps.rangeTo}
|
||||
globalFilters={alertSearchBarStateProps.filters ?? DEFAULT_FILTERS}
|
||||
globalFilters={[
|
||||
...(alertSearchBarStateProps.filters ?? DEFAULT_FILTERS),
|
||||
...filterControls,
|
||||
]}
|
||||
globalQuery={{ query: alertSearchBarStateProps.kuery, language: 'kuery' }}
|
||||
groupingId={ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID}
|
||||
defaultGroupingOptions={DEFAULT_GROUPING_OPTIONS}
|
||||
|
|
|
@ -13,10 +13,11 @@ import {
|
|||
EuiTabbedContent,
|
||||
EuiTabbedContentTab,
|
||||
} from '@elastic/eui';
|
||||
import { FilterGroupHandler } from '@kbn/alerts-ui-shared';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import type { Rule } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import type { Query, BoolQuery } from '@kbn/es-query';
|
||||
import type { BoolQuery, Filter } from '@kbn/es-query';
|
||||
import { ObservabilityAlertsTable } from '../../../components/alerts_table/alerts_table';
|
||||
import { observabilityAlertFeatureIds } from '../../../../common';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
@ -44,6 +45,7 @@ interface Props {
|
|||
ruleType: any;
|
||||
onEsQueryChange: (query: { bool: BoolQuery }) => void;
|
||||
onSetTabId: (tabId: TabId) => void;
|
||||
onControlApiAvailable?: (controlGroupHandler: FilterGroupHandler | undefined) => void;
|
||||
}
|
||||
|
||||
const tableColumns = getColumns();
|
||||
|
@ -57,13 +59,21 @@ export function RuleDetailsTabs({
|
|||
ruleType,
|
||||
onSetTabId,
|
||||
onEsQueryChange,
|
||||
onControlApiAvailable,
|
||||
}: Props) {
|
||||
const {
|
||||
triggersActionsUi: { getRuleEventLogList: RuleEventLogList },
|
||||
} = useKibana().services;
|
||||
|
||||
const ruleQuery = useRef<Query[]>([
|
||||
{ query: `kibana.alert.rule.uuid: ${ruleId}`, language: 'kuery' },
|
||||
const ruleFilters = useRef<Filter[]>([
|
||||
{
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.rule.uuid': ruleId,
|
||||
},
|
||||
},
|
||||
meta: {},
|
||||
},
|
||||
]);
|
||||
|
||||
const tabs: EuiTabbedContentTab[] = [
|
||||
|
@ -81,7 +91,9 @@ export function RuleDetailsTabs({
|
|||
appName={RULE_DETAILS_ALERTS_SEARCH_BAR_ID}
|
||||
onEsQueryChange={onEsQueryChange}
|
||||
urlStorageKey={RULE_DETAILS_SEARCH_BAR_URL_STORAGE_KEY}
|
||||
defaultSearchQueries={ruleQuery.current}
|
||||
defaultFilters={ruleFilters.current}
|
||||
disableLocalStorageSync={true}
|
||||
onControlApiAvailable={onControlApiAvailable}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
|
|
|
@ -7,43 +7,50 @@
|
|||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { DEFAULT_CONTROLS } from '@kbn/alerts-ui-shared/src/alert_filter_controls/constants';
|
||||
import type { FilterGroupHandler } from '@kbn/alerts-ui-shared';
|
||||
import { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleExecutionStatusErrorReasons } from '@kbn/alerting-plugin/common';
|
||||
import { RuleFormFlyout } from '@kbn/response-ops-rule-form/flyout';
|
||||
import type { BoolQuery } from '@kbn/es-query';
|
||||
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useFetchRule } from '../../hooks/use_fetch_rule';
|
||||
import { useFetchRuleTypes } from '../../hooks/use_fetch_rule_types';
|
||||
import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types';
|
||||
import { PageTitleContent } from './components/page_title_content';
|
||||
import { DeleteConfirmationModal } from './components/delete_confirmation_modal';
|
||||
import { CenterJustifiedSpinner } from '../../components/center_justified_spinner';
|
||||
import { NoRuleFoundPanel } from './components/no_rule_found_panel';
|
||||
import { HeaderActions } from './components/header_actions';
|
||||
import { RuleDetailsTabs } from './components/rule_details_tabs';
|
||||
import { getHealthColor } from './helpers/get_health_color';
|
||||
import { isRuleEditable } from './helpers/is_rule_editable';
|
||||
import { ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import { ruleDetailsLocatorID } from '../../../common';
|
||||
import {
|
||||
ALERT_STATUS_ALL,
|
||||
OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
|
||||
observabilityAlertFeatureIds,
|
||||
} from '../../../common/constants';
|
||||
import { paths } from '../../../common/locators/paths';
|
||||
import type { AlertStatus } from '../../../common/typings';
|
||||
import { RuleDetailsLocatorParams } from '../../locators/rule_details';
|
||||
import { getControlIndex } from '../../utils/alert_controls/get_control_index';
|
||||
import { updateSelectedOptions } from '../../utils/alert_controls/update_selected_options';
|
||||
import { setStatusOnControlConfigs } from '../../utils/alert_controls/set_status_on_control_configs';
|
||||
import { useKibana } from '../../utils/kibana_react';
|
||||
import { usePluginContext } from '../../hooks/use_plugin_context';
|
||||
import { useFetchRule } from '../../hooks/use_fetch_rule';
|
||||
import { useFetchRuleTypes } from '../../hooks/use_fetch_rule_types';
|
||||
import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types';
|
||||
import { CenterJustifiedSpinner } from '../../components/center_justified_spinner';
|
||||
import {
|
||||
defaultTimeRange,
|
||||
getDefaultAlertSummaryTimeRange,
|
||||
} from '../../utils/alert_summary_widget';
|
||||
import { PageTitleContent } from './components/page_title_content';
|
||||
import { DeleteConfirmationModal } from './components/delete_confirmation_modal';
|
||||
import { NoRuleFoundPanel } from './components/no_rule_found_panel';
|
||||
import { HeaderActions } from './components/header_actions';
|
||||
import { RuleDetailsTabs } from './components/rule_details_tabs';
|
||||
import { getHealthColor } from './helpers/get_health_color';
|
||||
import { isRuleEditable } from './helpers/is_rule_editable';
|
||||
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
|
||||
import {
|
||||
RULE_DETAILS_EXECUTION_TAB,
|
||||
RULE_DETAILS_ALERTS_TAB,
|
||||
RULE_DETAILS_TAB_URL_STORAGE_KEY,
|
||||
} from './constants';
|
||||
import { paths } from '../../../common/locators/paths';
|
||||
import {
|
||||
defaultTimeRange,
|
||||
getDefaultAlertSummaryTimeRange,
|
||||
} from '../../utils/alert_summary_widget';
|
||||
import type { AlertStatus } from '../../../common/typings';
|
||||
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
|
||||
|
||||
export type TabId = typeof RULE_DETAILS_ALERTS_TAB | typeof RULE_DETAILS_EXECUTION_TAB;
|
||||
|
||||
|
@ -99,6 +106,9 @@ export function RuleDetailsPage() {
|
|||
{ serverless }
|
||||
);
|
||||
|
||||
const [alertFilterControlHandler, setAlertFilterControlHandler] = useState<
|
||||
FilterGroupHandler | undefined
|
||||
>();
|
||||
const [activeTabId, setActiveTabId] = useState<TabId>(() => {
|
||||
const searchParams = new URLSearchParams(search);
|
||||
const urlTabId = searchParams.get(RULE_DETAILS_TAB_URL_STORAGE_KEY);
|
||||
|
@ -128,7 +138,7 @@ export function RuleDetailsPage() {
|
|||
const handleSetTabId = async (tabId: TabId) => {
|
||||
setActiveTabId(tabId);
|
||||
|
||||
await locators.get(ruleDetailsLocatorID)?.navigate(
|
||||
await locators.get<RuleDetailsLocatorParams>(ruleDetailsLocatorID)?.navigate(
|
||||
{
|
||||
ruleId,
|
||||
tabId,
|
||||
|
@ -141,13 +151,19 @@ export function RuleDetailsPage() {
|
|||
|
||||
const handleAlertSummaryWidgetClick = async (status: AlertStatus = ALERT_STATUS_ALL) => {
|
||||
setAlertSummaryWidgetTimeRange(getDefaultAlertSummaryTimeRange());
|
||||
const searchParams = new URLSearchParams(search);
|
||||
let controlConfigs: any = searchParams.get('controlConfigs') ?? DEFAULT_CONTROLS;
|
||||
|
||||
await locators.get(ruleDetailsLocatorID)?.navigate(
|
||||
const statusControlIndex = getControlIndex(ALERT_STATUS, controlConfigs);
|
||||
controlConfigs = setStatusOnControlConfigs(status, controlConfigs);
|
||||
updateSelectedOptions(status, statusControlIndex, alertFilterControlHandler);
|
||||
|
||||
await locators.get<RuleDetailsLocatorParams>(ruleDetailsLocatorID)?.navigate(
|
||||
{
|
||||
controlConfigs,
|
||||
rangeFrom: defaultTimeRange.from,
|
||||
rangeTo: defaultTimeRange.to,
|
||||
ruleId,
|
||||
status,
|
||||
tabId: RULE_DETAILS_ALERTS_TAB,
|
||||
},
|
||||
{
|
||||
|
@ -265,6 +281,7 @@ export function RuleDetailsPage() {
|
|||
activeTabId={activeTabId}
|
||||
onEsQueryChange={setEsQuery}
|
||||
onSetTabId={handleSetTabId}
|
||||
onControlApiAvailable={setAlertFilterControlHandler}
|
||||
/>
|
||||
|
||||
{isEditRuleFlyoutVisible && (
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
import React from 'react';
|
||||
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock';
|
||||
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { lensPluginMock } from '@kbn/lens-plugin/public/mocks';
|
||||
|
||||
|
@ -77,20 +79,6 @@ const dataViewEditor = {
|
|||
},
|
||||
};
|
||||
|
||||
const dataViews = {
|
||||
createStart() {
|
||||
return {
|
||||
getIds: jest.fn().mockImplementation(() => []),
|
||||
get: jest.fn(),
|
||||
create: jest.fn().mockImplementation(() => ({
|
||||
fields: {
|
||||
getByName: jest.fn(),
|
||||
},
|
||||
})),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const observabilityPublicPluginsStartMock = {
|
||||
createStart() {
|
||||
return {
|
||||
|
@ -99,11 +87,12 @@ export const observabilityPublicPluginsStartMock = {
|
|||
contentManagement: contentManagementMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
dataViewEditor: dataViewEditor.createStart(),
|
||||
dataViews: dataViews.createStart(),
|
||||
dataViews: dataViewPluginMocks.createStartContract(),
|
||||
discover: null,
|
||||
lens: lensPluginMock.createStartContract(),
|
||||
observabilityAIAssistant: observabilityAIAssistantPluginMock.createStartContract(),
|
||||
share: sharePluginMock.createStartContract(),
|
||||
spaces: spacesPluginMock.createStartContract(),
|
||||
triggersActionsUi: triggersActionsUiStartMock.createStart(),
|
||||
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_CONTROLS } from '@kbn/alerts-ui-shared/src/alert_filter_controls/constants';
|
||||
import { ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import { getControlIndex } from './get_control_index';
|
||||
|
||||
describe('getControlIndex()', () => {
|
||||
it('Should return correct index if the field name exist', () => {
|
||||
expect(getControlIndex(ALERT_STATUS, DEFAULT_CONTROLS)).toBe(0);
|
||||
});
|
||||
|
||||
it('Should return -1 if the field name does not exist', () => {
|
||||
expect(getControlIndex('nonexistent-fieldName', DEFAULT_CONTROLS)).toBe(-1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FilterControlConfig } from '@kbn/alerts-ui-shared';
|
||||
|
||||
export function getControlIndex(fieldName: string, controlConfigs: FilterControlConfig[]): number {
|
||||
return controlConfigs.findIndex((control) => control.fieldName === fieldName);
|
||||
}
|
|
@ -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
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DEFAULT_CONTROLS } from '@kbn/alerts-ui-shared/src/alert_filter_controls/constants';
|
||||
import { setStatusOnControlConfigs } from './set_status_on_control_configs';
|
||||
|
||||
describe('setStatusOnControlConfigs()', () => {
|
||||
it('Should return a default controlConfig with status if controlConfig is undefined', () => {
|
||||
const updatedControlConfigs = DEFAULT_CONTROLS;
|
||||
updatedControlConfigs[0].selectedOptions = ['recovered'];
|
||||
|
||||
expect(setStatusOnControlConfigs('recovered')).toEqual(updatedControlConfigs);
|
||||
});
|
||||
|
||||
it('Should return empty selectedOptions if status is ALL', () => {
|
||||
const updatedControlConfigs = DEFAULT_CONTROLS;
|
||||
updatedControlConfigs[0].selectedOptions = [];
|
||||
|
||||
expect(setStatusOnControlConfigs('all')).toEqual(updatedControlConfigs);
|
||||
});
|
||||
|
||||
it('Should return controlConfig with current selectedOptions when status is not the first item in config', () => {
|
||||
const controlConfigs = [DEFAULT_CONTROLS[1], DEFAULT_CONTROLS[0]];
|
||||
const updatedControlConfigs = controlConfigs;
|
||||
updatedControlConfigs[1].selectedOptions = ['active'];
|
||||
|
||||
expect(setStatusOnControlConfigs('active', controlConfigs)).toEqual(updatedControlConfigs);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FilterControlConfig } from '@kbn/alerts-ui-shared';
|
||||
import { DEFAULT_CONTROLS } from '@kbn/alerts-ui-shared/src/alert_filter_controls/constants';
|
||||
import { ALERT_STATUS } from '@kbn/rule-data-utils';
|
||||
import { ALERT_STATUS_ALL } from '../../../common/constants';
|
||||
import { AlertStatus } from '../../../common/typings';
|
||||
|
||||
export function setStatusOnControlConfigs(
|
||||
status: AlertStatus,
|
||||
controlConfigs?: FilterControlConfig[]
|
||||
) {
|
||||
const updateControlConfigs = controlConfigs ? [...controlConfigs] : DEFAULT_CONTROLS;
|
||||
const statusControl = updateControlConfigs.find((control) => control.fieldName === ALERT_STATUS);
|
||||
if (statusControl) {
|
||||
if (status === ALERT_STATUS_ALL) {
|
||||
statusControl.selectedOptions = [];
|
||||
} else {
|
||||
statusControl.selectedOptions = [status];
|
||||
}
|
||||
}
|
||||
|
||||
return updateControlConfigs;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FilterGroupHandler } from '@kbn/alerts-ui-shared';
|
||||
import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
|
||||
import { ALERT_STATUS_ALL } from '../../../common/constants';
|
||||
import { updateSelectedOptions } from './update_selected_options';
|
||||
|
||||
describe('updateSelectedOptions()', () => {
|
||||
const mockedClearSelections = jest.fn();
|
||||
const mockedSetSelectedOptions = jest.fn();
|
||||
const alertFilterControlHandler = {
|
||||
children$: {
|
||||
getValue: () => [
|
||||
{ clearSelections: mockedClearSelections, setSelectedOptions: mockedSetSelectedOptions },
|
||||
],
|
||||
},
|
||||
} as any as FilterGroupHandler;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Should not do anything if controlIndex is < 0', () => {
|
||||
updateSelectedOptions(ALERT_STATUS_ACTIVE, -1, alertFilterControlHandler);
|
||||
expect(mockedClearSelections).not.toHaveBeenCalled();
|
||||
expect(mockedSetSelectedOptions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should not do anything if alertFilterControlHandler does not exist', () => {
|
||||
updateSelectedOptions(ALERT_STATUS_ACTIVE, 0);
|
||||
expect(mockedClearSelections).not.toHaveBeenCalled();
|
||||
expect(mockedSetSelectedOptions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should clear selection if status is all', () => {
|
||||
updateSelectedOptions(ALERT_STATUS_ALL, 0, alertFilterControlHandler);
|
||||
expect(mockedClearSelections).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSetSelectedOptions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should change selected option is status is active', () => {
|
||||
updateSelectedOptions(ALERT_STATUS_ACTIVE, 0, alertFilterControlHandler);
|
||||
expect(mockedClearSelections).not.toHaveBeenCalled();
|
||||
expect(mockedSetSelectedOptions).toHaveBeenCalledTimes(1);
|
||||
expect(mockedSetSelectedOptions).toHaveBeenCalledWith(['active']);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { FilterGroupHandler } from '@kbn/alerts-ui-shared';
|
||||
import { OptionsListControlApi } from '@kbn/controls-plugin/public/controls/data_controls/options_list_control/types';
|
||||
import { DefaultControlApi } from '@kbn/controls-plugin/public/controls/types';
|
||||
import { ALERT_STATUS_ALL } from '../../../common/constants';
|
||||
import { AlertStatus } from '../../../common/typings';
|
||||
|
||||
export function updateSelectedOptions(
|
||||
status: AlertStatus,
|
||||
controlIndex: number,
|
||||
alertFilterControlHandler?: FilterGroupHandler
|
||||
) {
|
||||
if (!alertFilterControlHandler || controlIndex < 0) {
|
||||
return;
|
||||
}
|
||||
if (status === ALERT_STATUS_ALL) {
|
||||
const controlApi = alertFilterControlHandler?.children$.getValue()[
|
||||
controlIndex
|
||||
] as DefaultControlApi;
|
||||
controlApi?.clearSelections?.();
|
||||
} else {
|
||||
const controlApi = alertFilterControlHandler?.children$.getValue()[
|
||||
controlIndex
|
||||
] as Partial<OptionsListControlApi>;
|
||||
if (controlApi && controlApi.setSelectedOptions) {
|
||||
controlApi.setSelectedOptions([status]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -117,7 +117,9 @@
|
|||
"@kbn/data-service",
|
||||
"@kbn/ebt-tools",
|
||||
"@kbn/response-ops-rule-params",
|
||||
"@kbn/fields-metadata-plugin"
|
||||
"@kbn/fields-metadata-plugin",
|
||||
"@kbn/controls-plugin",
|
||||
"@kbn/core-http-browser"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
96
x-pack/test/functional/page_objects/alert_controls.ts
Normal file
96
x-pack/test/functional/page_objects/alert_controls.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
export function AlertControlsProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const find = getService('find');
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
|
||||
return {
|
||||
async getControlElementById(controlId: string): Promise<WebElementWrapper> {
|
||||
const errorText = `Control frame ${controlId} could not be found`;
|
||||
let controlElement: WebElementWrapper | undefined;
|
||||
await retry.try(async () => {
|
||||
const controlFrames = await testSubjects.findAll('control-frame');
|
||||
const framesWithIds = await Promise.all(
|
||||
controlFrames.map(async (frame) => {
|
||||
const id = await frame.getAttribute('data-control-id');
|
||||
return { id, element: frame };
|
||||
})
|
||||
);
|
||||
const foundControlFrame = framesWithIds.find(({ id }) => id === controlId);
|
||||
if (!foundControlFrame) throw new Error(errorText);
|
||||
controlElement = foundControlFrame.element;
|
||||
});
|
||||
if (!controlElement) throw new Error(errorText);
|
||||
return controlElement;
|
||||
},
|
||||
|
||||
async hoverOverExistingControl(controlId: string) {
|
||||
const elementToHover = await this.getControlElementById(controlId);
|
||||
await retry.try(async () => {
|
||||
await elementToHover.moveMouseTo();
|
||||
await testSubjects.existOrFail(`control-action-${controlId}-erase`);
|
||||
});
|
||||
},
|
||||
|
||||
async clearControlSelections(controlId: string) {
|
||||
log.debug(`clearing all selections from control ${controlId}`);
|
||||
await this.hoverOverExistingControl(controlId);
|
||||
await testSubjects.click(`control-action-${controlId}-erase`);
|
||||
},
|
||||
|
||||
async optionsListOpenPopover(controlId: string) {
|
||||
log.debug(`Opening popover for Options List: ${controlId}`);
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click(`optionsList-control-${controlId}`);
|
||||
await retry.waitForWithTimeout('popover to open', 500, async () => {
|
||||
return await testSubjects.exists(`optionsList-control-popover`);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async optionsListPopoverAssertOpen() {
|
||||
await retry.try(async () => {
|
||||
if (!(await testSubjects.exists(`optionsList-control-available-options`))) {
|
||||
throw new Error('options list popover must be open before calling selectOption');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async optionsListPopoverSelectOption(availableOption: string) {
|
||||
log.debug(`selecting ${availableOption} from options list`);
|
||||
await this.optionsListPopoverAssertOpen();
|
||||
|
||||
await retry.try(async () => {
|
||||
await testSubjects.existOrFail(`optionsList-control-selection-${availableOption}`);
|
||||
await testSubjects.click(`optionsList-control-selection-${availableOption}`);
|
||||
});
|
||||
},
|
||||
|
||||
async isOptionsListPopoverOpen(controlId: string) {
|
||||
const isPopoverOpen = await find.existsByCssSelector(`#control-popover-${controlId}`);
|
||||
log.debug(`Is popover open: ${isPopoverOpen} for Options List: ${controlId}`);
|
||||
return isPopoverOpen;
|
||||
},
|
||||
|
||||
async optionsListEnsurePopoverIsClosed(controlId: string) {
|
||||
log.debug(`Ensure popover is closed for Options List: ${controlId}`);
|
||||
await retry.try(async () => {
|
||||
const isPopoverOpen = await this.isOptionsListPopoverOpen(controlId);
|
||||
if (isPopoverOpen) {
|
||||
await testSubjects.click(`optionsList-control-${controlId}`);
|
||||
await testSubjects.waitForDeleted(`optionsList-control-available-options`);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -37,6 +37,7 @@ import { NavigationalSearchPageObject } from './navigational_search';
|
|||
import { ObservabilityLogsExplorerPageObject } from './observability_logs_explorer';
|
||||
import { DatasetQualityPageObject } from './dataset_quality';
|
||||
import { ObservabilityPageProvider } from './observability_page';
|
||||
import { AlertControlsProvider } from './alert_controls';
|
||||
import { RemoteClustersPageProvider } from './remote_clusters_page';
|
||||
import { ReportingPageObject } from './reporting_page';
|
||||
import { RoleMappingsPageProvider } from './role_mappings_page';
|
||||
|
@ -96,6 +97,7 @@ export const pageObjects = {
|
|||
observabilityLogsExplorer: ObservabilityLogsExplorerPageObject,
|
||||
datasetQuality: DatasetQualityPageObject,
|
||||
observability: ObservabilityPageProvider,
|
||||
alertControls: AlertControlsProvider,
|
||||
remoteClusters: RemoteClustersPageProvider,
|
||||
reporting: ReportingPageObject,
|
||||
roleMappings: RoleMappingsPageProvider,
|
||||
|
|
|
@ -21,11 +21,13 @@ const DATE_WITH_DATA = {
|
|||
|
||||
const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout';
|
||||
const FILTER_FOR_VALUE_BUTTON_SELECTOR = 'filterForValue';
|
||||
const ALERTS_TABLE_CONTAINER_SELECTOR = 'alertsTable';
|
||||
const ALERTS_TABLE_WITH_DATA_SELECTOR = 'alertsTable';
|
||||
const ALERTS_TABLE_NO_DATA_SELECTOR = 'alertsTableEmptyState';
|
||||
const ALERTS_TABLE_ERROR_PROMPT_SELECTOR = 'alertsTableErrorPrompt';
|
||||
const ALERTS_TABLE_ACTIONS_MENU_SELECTOR = 'alertsTableActionsMenu';
|
||||
const VIEW_RULE_DETAILS_SELECTOR = 'viewRuleDetails';
|
||||
const VIEW_RULE_DETAILS_FLYOUT_SELECTOR = 'viewRuleDetailsFlyout';
|
||||
const ALERTS_TABLE_LOADING_SELECTOR = 'internalAlertsPageLoading';
|
||||
|
||||
type WorkflowStatus = 'open' | 'acknowledged' | 'closed';
|
||||
|
||||
|
@ -36,19 +38,20 @@ export function ObservabilityAlertsCommonProvider({
|
|||
const find = getService('find');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const flyoutService = getService('flyout');
|
||||
const pageObjects = getPageObjects(['common']);
|
||||
const retry = getService('retry');
|
||||
const toasts = getService('toasts');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const retryOnStale = getService('retryOnStale');
|
||||
const pageObjects = getPageObjects(['common', 'header']);
|
||||
|
||||
const navigateToTimeWithData = async () => {
|
||||
return await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
await pageObjects.common.navigateToUrlWithBrowserHistory(
|
||||
'observability',
|
||||
'/alerts',
|
||||
`?_a=(rangeFrom:'${DATE_WITH_DATA.rangeFrom}',rangeTo:'${DATE_WITH_DATA.rangeTo}')`,
|
||||
{ ensureCurrentUrl: false }
|
||||
);
|
||||
await pageObjects.header.waitUntilLoadingHasFinished();
|
||||
};
|
||||
|
||||
const navigateToRulesPage = async () => {
|
||||
|
@ -102,8 +105,23 @@ export function ObservabilityAlertsCommonProvider({
|
|||
});
|
||||
};
|
||||
|
||||
// Alert table
|
||||
const waitForAlertsTableLoadingToDisappear = async () => {
|
||||
await testSubjects.missingOrFail(ALERTS_TABLE_LOADING_SELECTOR, { timeout: 30_000 });
|
||||
};
|
||||
|
||||
const waitForAlertTableToLoad = async () => {
|
||||
await waitForAlertsTableLoadingToDisappear();
|
||||
await retry.waitFor('alerts table to appear', async () => {
|
||||
return (
|
||||
(await testSubjects.exists(ALERTS_TABLE_NO_DATA_SELECTOR)) ||
|
||||
(await testSubjects.exists(ALERTS_TABLE_WITH_DATA_SELECTOR))
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getTableColumnHeaders = async () => {
|
||||
const table = await testSubjects.find(ALERTS_TABLE_CONTAINER_SELECTOR);
|
||||
const table = await testSubjects.find(ALERTS_TABLE_WITH_DATA_SELECTOR);
|
||||
const tableHeaderRow = await testSubjects.findDescendant('dataGridHeader', table);
|
||||
const columnHeaders = await tableHeaderRow.findAllByXpath('./div');
|
||||
return columnHeaders;
|
||||
|
@ -132,7 +150,7 @@ export function ObservabilityAlertsCommonProvider({
|
|||
});
|
||||
|
||||
const getTableOrFail = async () => {
|
||||
return await testSubjects.existOrFail(ALERTS_TABLE_CONTAINER_SELECTOR);
|
||||
return await testSubjects.existOrFail(ALERTS_TABLE_WITH_DATA_SELECTOR);
|
||||
};
|
||||
|
||||
const ensureNoTableErrorPrompt = async () => {
|
||||
|
@ -149,6 +167,9 @@ export function ObservabilityAlertsCommonProvider({
|
|||
|
||||
// Query Bar
|
||||
const getQueryBar = async () => {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.existOrFail('queryInput');
|
||||
});
|
||||
return await testSubjects.find('queryInput');
|
||||
};
|
||||
|
||||
|
@ -409,6 +430,7 @@ export function ObservabilityAlertsCommonProvider({
|
|||
getTableCellsInRows,
|
||||
getTableColumnHeaders,
|
||||
getTableOrFail,
|
||||
waitForAlertTableToLoad,
|
||||
ensureNoTableErrorPrompt,
|
||||
navigateToTimeWithData,
|
||||
setKibanaTimeZoneToUTC,
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('ObservabilityApp', function () {
|
||||
loadTestFile(require.resolve('./pages/alerts'));
|
||||
loadTestFile(require.resolve('./pages/alerts/add_to_case'));
|
||||
loadTestFile(require.resolve('./pages/alerts/alert_status'));
|
||||
loadTestFile(require.resolve('./pages/alerts/alert_controls'));
|
||||
loadTestFile(require.resolve('./pages/alerts/alert_summary_widget'));
|
||||
loadTestFile(require.resolve('./pages/alerts/pagination'));
|
||||
loadTestFile(require.resolve('./pages/alerts/rule_stats'));
|
||||
|
|
|
@ -6,17 +6,17 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { ALERT_STATUS_RECOVERED, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
const ALL_ALERTS = 40;
|
||||
const ACTIVE_ALERTS = 10;
|
||||
const RECOVERED_ALERTS = 30;
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const { alertControls, header } = getPageObjects(['alertControls', 'header']);
|
||||
|
||||
describe('Alert status filter >', function () {
|
||||
describe('Alert controls >', function () {
|
||||
this.tags('includeFirefox');
|
||||
|
||||
const observability = getService('observability');
|
||||
|
@ -33,35 +33,32 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
|
||||
});
|
||||
|
||||
it('is filtered to only show "all" alerts by default', async () => {
|
||||
await retry.try(async () => {
|
||||
const tableRows = await observability.alerts.common.getTableCellsInRows();
|
||||
expect(tableRows.length).to.be(ALL_ALERTS);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be filtered to only show "active" alerts using the filter button', async () => {
|
||||
await observability.alerts.common.setAlertStatusFilter(ALERT_STATUS_ACTIVE);
|
||||
it('is filtered to only show "active" alerts by default', async () => {
|
||||
await retry.try(async () => {
|
||||
const tableRows = await observability.alerts.common.getTableCellsInRows();
|
||||
expect(tableRows.length).to.be(ACTIVE_ALERTS);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be filtered to only show "recovered" alerts using the filter button', async () => {
|
||||
await observability.alerts.common.setAlertStatusFilter(ALERT_STATUS_RECOVERED);
|
||||
await retry.try(async () => {
|
||||
const tableRows = await observability.alerts.common.getTableCellsInRows();
|
||||
expect(tableRows.length).to.be(RECOVERED_ALERTS);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be filtered to only show "all" alerts using the filter button', async () => {
|
||||
await observability.alerts.common.setAlertStatusFilter();
|
||||
it('can be filtered to only show "all" when filter is cleared', async () => {
|
||||
// Clear status filter
|
||||
await alertControls.clearControlSelections('0');
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
await retry.try(async () => {
|
||||
const tableRows = await observability.alerts.common.getTableCellsInRows();
|
||||
expect(tableRows.length).to.be(ALL_ALERTS);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be filtered to only show "recovered" alerts using the filter button', async () => {
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
await alertControls.optionsListOpenPopover('0');
|
||||
await alertControls.optionsListPopoverSelectOption('recovered');
|
||||
await alertControls.optionsListEnsurePopoverIsClosed('0');
|
||||
await retry.try(async () => {
|
||||
const tableRows = await observability.alerts.common.getTableCellsInRows();
|
||||
expect(tableRows.length).to.be(RECOVERED_ALERTS);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -11,8 +11,10 @@ import { FtrProviderContext } from '../../../../ftr_provider_context';
|
|||
const ALL_ALERTS = 40;
|
||||
const ACTIVE_ALERTS = 10;
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const retry = getService('retry');
|
||||
const { alertControls, header } = getPageObjects(['alertControls', 'header']);
|
||||
|
||||
describe('Alert summary widget >', function () {
|
||||
this.tags('includeFirefox');
|
||||
|
@ -30,16 +32,38 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
|
||||
});
|
||||
|
||||
it('shows number of total and active alerts', async () => {
|
||||
it('shows number of total and active alerts when status filter is active', async () => {
|
||||
await observability.components.alertSummaryWidget.getFullSizeComponentSelectorOrFail();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
const activeAlertCount =
|
||||
await observability.components.alertSummaryWidget.getActiveAlertCount();
|
||||
const totalAlertCount =
|
||||
await observability.components.alertSummaryWidget.getTotalAlertCount();
|
||||
await retry.try(async () => {
|
||||
const activeAlertCount =
|
||||
await observability.components.alertSummaryWidget.getActiveAlertCount();
|
||||
const totalAlertCount =
|
||||
await observability.components.alertSummaryWidget.getTotalAlertCount();
|
||||
|
||||
expect(activeAlertCount).to.be(`${ACTIVE_ALERTS} `);
|
||||
expect(totalAlertCount).to.be(`${ALL_ALERTS}`);
|
||||
expect(activeAlertCount).to.be(`${ACTIVE_ALERTS} `);
|
||||
expect(totalAlertCount).to.be(`${ACTIVE_ALERTS}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows number of total and active alerts when there is no status filter', async () => {
|
||||
// Clear status filter
|
||||
await alertControls.clearControlSelections('0');
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
|
||||
await observability.components.alertSummaryWidget.getFullSizeComponentSelectorOrFail();
|
||||
await header.waitUntilLoadingHasFinished();
|
||||
|
||||
await retry.try(async () => {
|
||||
const activeAlertCount =
|
||||
await observability.components.alertSummaryWidget.getActiveAlertCount();
|
||||
const totalAlertCount =
|
||||
await observability.components.alertSummaryWidget.getTotalAlertCount();
|
||||
|
||||
expect(activeAlertCount).to.be(`${ACTIVE_ALERTS} `);
|
||||
expect(totalAlertCount).to.be(`${ALL_ALERTS}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -9,16 +9,18 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
import { asyncForEach } from '../../helpers';
|
||||
|
||||
const ACTIVE_ALERTS_CELL_COUNT = 78;
|
||||
const RECOVERED_ALERTS_CELL_COUNT = 330;
|
||||
const INFRA_ACTIVE_ALERTS_CELL_COUNT = 78;
|
||||
const TOTAL_ALERTS_CELL_COUNT = 440;
|
||||
const RECOVERED_ALERTS_CELL_COUNT = 330;
|
||||
const ACTIVE_ALERTS_CELL_COUNT = 110;
|
||||
|
||||
const DISABLED_ALERTS_CHECKBOX = 6;
|
||||
const ENABLED_ALERTS_CHECKBOX = 4;
|
||||
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const find = getService('find');
|
||||
const { alertControls } = getPageObjects(['alertControls']);
|
||||
|
||||
describe('Observability alerts >', function () {
|
||||
this.tags('includeFirefox');
|
||||
|
@ -55,7 +57,19 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
await observability.alerts.common.getTableOrFail();
|
||||
});
|
||||
|
||||
it('Renders the correct number of cells', async () => {
|
||||
it('Renders the correct number of cells (active alerts)', async () => {
|
||||
await retry.try(async () => {
|
||||
const cells = await observability.alerts.common.getTableCells();
|
||||
expect(cells.length).to.be(ACTIVE_ALERTS_CELL_COUNT);
|
||||
});
|
||||
});
|
||||
|
||||
it('Clear status control', async () => {
|
||||
await alertControls.clearControlSelections('0');
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
});
|
||||
|
||||
it('Renders the correct number of cells (all alerts)', async () => {
|
||||
await retry.try(async () => {
|
||||
const cells = await observability.alerts.common.getTableCells();
|
||||
expect(cells.length).to.be(TOTAL_ALERTS_CELL_COUNT);
|
||||
|
@ -104,6 +118,9 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
describe('Date selection', () => {
|
||||
after(async () => {
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
// Clear active status
|
||||
await alertControls.clearControlSelections('0');
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
});
|
||||
|
||||
it('Correctly applies date picker selections', async () => {
|
||||
|
@ -218,7 +235,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
// Wait for request
|
||||
await retry.try(async () => {
|
||||
const cells = await observability.alerts.common.getTableCells();
|
||||
expect(cells.length).to.be(ACTIVE_ALERTS_CELL_COUNT);
|
||||
expect(cells.length).to.be(INFRA_ACTIVE_ALERTS_CELL_COUNT);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,11 +17,10 @@ import {
|
|||
ALERT_STATUS,
|
||||
ALERT_INSTANCE_ID,
|
||||
TAGS,
|
||||
ALERT_STATUS_ACTIVE,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { FtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default ({ getService, getPageObject }: FtrProviderContext) => {
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
describe('Observability alerts table configuration', function () {
|
||||
this.tags('includeFirefox');
|
||||
|
||||
|
@ -56,6 +55,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
|
|||
|
||||
it('renders the correct columns', async () => {
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
for (const colId of [
|
||||
ALERT_STATUS,
|
||||
ALERT_START,
|
||||
|
@ -73,12 +73,13 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
|
|||
|
||||
it('renders the group selector', async () => {
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
expect(await testSubjects.exists('group-selector-dropdown')).to.be(true);
|
||||
});
|
||||
|
||||
it('renders the correct alert actions', async () => {
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
await observability.alerts.common.setAlertStatusFilter(ALERT_STATUS_ACTIVE);
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
await testSubjects.click('alertsTableRowActionMore');
|
||||
await retry.waitFor('alert actions popover visible', () =>
|
||||
testSubjects.exists('alertsTableActionsMenu')
|
||||
|
@ -97,6 +98,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
|
|||
|
||||
it('remembers column changes', async () => {
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
await dataGrid.clickHideColumn('kibana.alert.duration.us');
|
||||
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
|
@ -109,6 +111,7 @@ export default ({ getService, getPageObject }: FtrProviderContext) => {
|
|||
|
||||
it('remembers sorting changes', async () => {
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
await observability.alerts.common.waitForAlertTableToLoad();
|
||||
await dataGrid.clickDocSortAsc('kibana.alert.start');
|
||||
|
||||
await observability.alerts.common.navigateToTimeWithData();
|
||||
|
|
|
@ -172,9 +172,10 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const url = await browser.getCurrentUrl();
|
||||
const from = 'rangeFrom:now-30d';
|
||||
const to = 'rangeTo:now';
|
||||
const status = 'selectedOptions:!(active),title:Status';
|
||||
|
||||
expect(url.includes('tabId=alerts')).to.be(true);
|
||||
expect(url.includes('status%3Aactive')).to.be(true);
|
||||
expect(url.includes(status.replaceAll(':', '%3A').replaceAll(',', '%2C'))).to.be(true);
|
||||
expect(url.includes(from.replaceAll(':', '%3A'))).to.be(true);
|
||||
expect(url.includes(to.replaceAll(':', '%3A'))).to.be(true);
|
||||
});
|
||||
|
@ -189,9 +190,10 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const url = await browser.getCurrentUrl();
|
||||
const from = 'rangeFrom:now-30d';
|
||||
const to = 'rangeTo:now';
|
||||
const status = 'selectedOptions:!(),title:Status';
|
||||
|
||||
expect(url.includes('tabId=alerts')).to.be(true);
|
||||
expect(url.includes('status%3Aall')).to.be(true);
|
||||
expect(url.includes(status.replaceAll(':', '%3A').replaceAll(',', '%2C'))).to.be(true);
|
||||
expect(url.includes(from.replaceAll(':', '%3A'))).to.be(true);
|
||||
expect(url.includes(to.replaceAll(':', '%3A'))).to.be(true);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue