mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.12`: - [[RAM] Stack Management::Rules loses user selections when navigating back (#174954)](https://github.com/elastic/kibana/pull/174954) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Xavier Mouligneau","email":"xavier.mouligneau@elastic.co"},"sourceCommit":{"committedDate":"2024-01-24T23:01:53Z","message":"[RAM] Stack Management::Rules loses user selections when navigating back (#174954)\n\n## Summary\r\n\r\nFIX =>dd6a2b83
-7460-4d90-8ee4-39c40534c247)\r\n\r\n\r\n### Checklist\r\n\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"9357bd4714ef0af35d75534b5d19d33684e0aaaf","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:enhancement","Team:ResponseOps","Feature:Alerting/RulesManagement","v8.12.1","v8.13.0"],"title":"[RAM] Stack Management::Rules loses user selections when navigating back","number":174954,"url":"https://github.com/elastic/kibana/pull/174954","mergeCommit":{"message":"[RAM] Stack Management::Rules loses user selections when navigating back (#174954)\n\n## Summary\r\n\r\nFIX =>dd6a2b83
-7460-4d90-8ee4-39c40534c247)\r\n\r\n\r\n### Checklist\r\n\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"9357bd4714ef0af35d75534b5d19d33684e0aaaf"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","branchLabelMappingKey":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/174954","number":174954,"mergeCommit":{"message":"[RAM] Stack Management::Rules loses user selections when navigating back (#174954)\n\n## Summary\r\n\r\nFIX =>dd6a2b83
-7460-4d90-8ee4-39c40534c247)\r\n\r\n\r\n### Checklist\r\n\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"9357bd4714ef0af35d75534b5d19d33684e0aaaf"}}]}] BACKPORT--> Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
de22341254
commit
5c7f3729fd
11 changed files with 619 additions and 56 deletions
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* 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 { act, renderHook } from '@testing-library/react-hooks';
|
||||
import * as useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { useRulesListFilterStore } from './use_rules_list_filter_store';
|
||||
|
||||
jest.mock('@kbn/kibana-utils-plugin/public');
|
||||
const { createKbnUrlStateStorage } = jest.requireMock('@kbn/kibana-utils-plugin/public');
|
||||
|
||||
const useUrlStateStorageGetMock = jest.fn();
|
||||
const useUrlStateStorageSetMock = jest.fn();
|
||||
const setRulesListFilterLocalMock = jest.fn();
|
||||
const LOCAL_STORAGE_KEY = 'test_local';
|
||||
describe('useRulesListFilterStore', () => {
|
||||
beforeAll(() => {
|
||||
createKbnUrlStateStorage.mockReturnValue({
|
||||
get: useUrlStateStorageGetMock,
|
||||
set: useUrlStateStorageSetMock,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useLocalStorage, 'default')
|
||||
.mockImplementation(() => [null, setRulesListFilterLocalMock, () => {}]);
|
||||
useUrlStateStorageGetMock.mockReturnValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Should return empty filter when url query param and local storage and props are empty', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRulesListFilterStore({
|
||||
rulesListKey: LOCAL_STORAGE_KEY,
|
||||
})
|
||||
);
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: [],
|
||||
kueryNode: undefined,
|
||||
ruleExecutionStatuses: [],
|
||||
ruleLastRunOutcomes: [],
|
||||
ruleParams: {},
|
||||
ruleStatuses: [],
|
||||
searchText: '',
|
||||
tags: [],
|
||||
types: [],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(0);
|
||||
});
|
||||
|
||||
it('Should return the props as filter when url query param and local storage are empty', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRulesListFilterStore({
|
||||
lastResponseFilter: ['props-lastResponse-filter'],
|
||||
lastRunOutcomeFilter: ['props-lastRunOutcome-filter'],
|
||||
rulesListKey: LOCAL_STORAGE_KEY,
|
||||
ruleParamFilter: { propsRuleParams: 'props-ruleParams-filter' },
|
||||
statusFilter: ['enabled'],
|
||||
searchFilter: 'props-search-filter',
|
||||
typeFilter: ['props-ruleType-filter'],
|
||||
})
|
||||
);
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: [],
|
||||
kueryNode: undefined,
|
||||
ruleExecutionStatuses: ['props-lastResponse-filter'],
|
||||
ruleLastRunOutcomes: ['props-lastRunOutcome-filter'],
|
||||
ruleParams: {
|
||||
propsRuleParams: 'props-ruleParams-filter',
|
||||
},
|
||||
ruleStatuses: ['enabled'],
|
||||
searchText: 'props-search-filter',
|
||||
tags: [],
|
||||
types: ['props-ruleType-filter'],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(6);
|
||||
});
|
||||
|
||||
it('Should return the local storage params as filter when url query param is empty', () => {
|
||||
jest.spyOn(useLocalStorage, 'default').mockImplementation(() => [
|
||||
{
|
||||
actionTypes: ['localStorage-actionType-filter'],
|
||||
lastResponse: ['localStorage-lastResponse-filter'],
|
||||
params: { localStorageRuleParams: 'localStorage-ruleParams-filter' },
|
||||
search: 'localStorage-search-filter',
|
||||
status: ['disabled'],
|
||||
tags: ['localStorage-tag-filter'],
|
||||
type: ['localStorage-ruleType-filter'],
|
||||
},
|
||||
() => null,
|
||||
() => {},
|
||||
]);
|
||||
const { result } = renderHook(() =>
|
||||
useRulesListFilterStore({
|
||||
lastResponseFilter: ['props-lastResponse-filter'],
|
||||
lastRunOutcomeFilter: ['props-lastRunOutcome-filter'],
|
||||
rulesListKey: LOCAL_STORAGE_KEY,
|
||||
ruleParamFilter: { propsRuleParams: 'props-ruleParams-filter' },
|
||||
statusFilter: ['enabled'],
|
||||
searchFilter: 'props-search-filter',
|
||||
typeFilter: ['ruleType-filter'],
|
||||
})
|
||||
);
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: ['localStorage-actionType-filter'],
|
||||
kueryNode: undefined,
|
||||
// THIS is valid because we are not using this param in local storage
|
||||
ruleExecutionStatuses: ['props-lastResponse-filter'],
|
||||
ruleLastRunOutcomes: ['localStorage-lastResponse-filter'],
|
||||
ruleParams: {
|
||||
localStorageRuleParams: 'localStorage-ruleParams-filter',
|
||||
},
|
||||
ruleStatuses: ['disabled'],
|
||||
searchText: 'localStorage-search-filter',
|
||||
tags: ['localStorage-tag-filter'],
|
||||
types: ['localStorage-ruleType-filter'],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(8);
|
||||
});
|
||||
|
||||
it('Should return the url params as filter when url query param is empty', () => {
|
||||
jest.spyOn(useLocalStorage, 'default').mockImplementation(() => [
|
||||
{
|
||||
actionTypes: ['localStorage-actionType-filter'],
|
||||
lastResponse: ['localStorage-lastResponse-filter'],
|
||||
params: { localStorageRuleParams: 'localStorage-ruleParams-filter' },
|
||||
search: 'localStorage-search-filter',
|
||||
status: ['disabled'],
|
||||
tags: ['localStorage-tag-filter'],
|
||||
type: ['localStorage-ruleType-filter'],
|
||||
},
|
||||
() => null,
|
||||
() => {},
|
||||
]);
|
||||
useUrlStateStorageGetMock.mockReturnValue({
|
||||
actionTypes: ['urlQueryParams-actionType-filter'],
|
||||
lastResponse: ['urlQueryParams-lastResponse-filter'],
|
||||
params: { urlQueryParamsRuleParams: 'urlQueryParams-ruleParams-filter' },
|
||||
search: 'urlQueryParams-search-filter',
|
||||
status: ['snoozed'],
|
||||
tags: ['urlQueryParams-tag-filter'],
|
||||
type: ['urlQueryParams-ruleType-filter'],
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useRulesListFilterStore({
|
||||
lastResponseFilter: ['props-lastResponse-filter'],
|
||||
lastRunOutcomeFilter: ['props-lastRunOutcome-filter'],
|
||||
rulesListKey: LOCAL_STORAGE_KEY,
|
||||
ruleParamFilter: { propsRuleParams: 'props-ruleParams-filter' },
|
||||
statusFilter: ['enabled'],
|
||||
searchFilter: 'props-search-filter',
|
||||
typeFilter: ['ruleType-filter'],
|
||||
})
|
||||
);
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: ['urlQueryParams-actionType-filter'],
|
||||
kueryNode: undefined,
|
||||
// THIS is valid because we are not using this param in url query params
|
||||
ruleExecutionStatuses: ['props-lastResponse-filter'],
|
||||
ruleLastRunOutcomes: ['urlQueryParams-lastResponse-filter'],
|
||||
ruleParams: {
|
||||
urlQueryParamsRuleParams: 'urlQueryParams-ruleParams-filter',
|
||||
},
|
||||
ruleStatuses: ['snoozed'],
|
||||
searchText: 'urlQueryParams-search-filter',
|
||||
tags: ['urlQueryParams-tag-filter'],
|
||||
types: ['urlQueryParams-ruleType-filter'],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(8);
|
||||
});
|
||||
|
||||
it('Should clear filter when resetFiltersStore has been called', async () => {
|
||||
useUrlStateStorageGetMock.mockReturnValue({
|
||||
actionTypes: ['urlQueryParams-actionType-filter'],
|
||||
lastResponse: ['urlQueryParams-lastResponse-filter'],
|
||||
params: { urlQueryParamsRuleParams: 'urlQueryParams-ruleParams-filter' },
|
||||
search: 'urlQueryParams-search-filter',
|
||||
status: ['snoozed'],
|
||||
tags: ['urlQueryParams-tag-filter'],
|
||||
type: ['urlQueryParams-ruleType-filter'],
|
||||
});
|
||||
const { result } = renderHook(() =>
|
||||
useRulesListFilterStore({
|
||||
rulesListKey: LOCAL_STORAGE_KEY,
|
||||
})
|
||||
);
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: ['urlQueryParams-actionType-filter'],
|
||||
kueryNode: undefined,
|
||||
ruleExecutionStatuses: [],
|
||||
ruleLastRunOutcomes: ['urlQueryParams-lastResponse-filter'],
|
||||
ruleParams: {
|
||||
urlQueryParamsRuleParams: 'urlQueryParams-ruleParams-filter',
|
||||
},
|
||||
ruleStatuses: ['snoozed'],
|
||||
searchText: 'urlQueryParams-search-filter',
|
||||
tags: ['urlQueryParams-tag-filter'],
|
||||
types: ['urlQueryParams-ruleType-filter'],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(7);
|
||||
|
||||
act(() => {
|
||||
result.current.resetFiltersStore();
|
||||
});
|
||||
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: [],
|
||||
kueryNode: undefined,
|
||||
ruleExecutionStatuses: [],
|
||||
ruleLastRunOutcomes: [],
|
||||
ruleParams: {},
|
||||
ruleStatuses: [],
|
||||
searchText: '',
|
||||
tags: [],
|
||||
types: [],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(0);
|
||||
expect(useUrlStateStorageSetMock).toBeCalledTimes(1);
|
||||
expect(setRulesListFilterLocalMock).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Should set filter when setFiltersStore has been called', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
useRulesListFilterStore({
|
||||
rulesListKey: LOCAL_STORAGE_KEY,
|
||||
})
|
||||
);
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: [],
|
||||
kueryNode: undefined,
|
||||
ruleExecutionStatuses: [],
|
||||
ruleLastRunOutcomes: [],
|
||||
ruleParams: {},
|
||||
ruleStatuses: [],
|
||||
searchText: '',
|
||||
tags: [],
|
||||
types: [],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(0);
|
||||
|
||||
act(() => {
|
||||
result.current.setFiltersStore({ filter: 'tags', value: ['my-tags'] });
|
||||
});
|
||||
|
||||
expect(result.current.filters).toEqual({
|
||||
actionTypes: [],
|
||||
kueryNode: undefined,
|
||||
ruleExecutionStatuses: [],
|
||||
ruleLastRunOutcomes: [],
|
||||
ruleParams: {},
|
||||
ruleStatuses: [],
|
||||
searchText: '',
|
||||
tags: ['my-tags'],
|
||||
types: [],
|
||||
});
|
||||
expect(result.current.numberOfFiltersStore).toEqual(1);
|
||||
expect(useUrlStateStorageSetMock).toBeCalledTimes(1);
|
||||
expect(setRulesListFilterLocalMock).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 { useHistory } from 'react-router-dom';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import useLocalStorage from 'react-use/lib/useLocalStorage';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { RuleStatus } from '../../../../../common';
|
||||
import { RulesListFilters, RulesListProps, UpdateFiltersProps } from '../../../../../types';
|
||||
|
||||
type FilterStoreProps = Pick<
|
||||
RulesListProps,
|
||||
| 'lastResponseFilter'
|
||||
| 'lastRunOutcomeFilter'
|
||||
| 'rulesListKey'
|
||||
| 'ruleParamFilter'
|
||||
| 'statusFilter'
|
||||
| 'searchFilter'
|
||||
| 'typeFilter'
|
||||
>;
|
||||
const RULES_LIST_FILTERS_KEY = 'triggersActionsUi_rulesList';
|
||||
|
||||
interface FilterParameters {
|
||||
actionTypes?: string[];
|
||||
lastResponse?: string[];
|
||||
params?: Record<string, string | number | object>;
|
||||
search?: string;
|
||||
status?: RuleStatus[];
|
||||
tags?: string[];
|
||||
type?: string[];
|
||||
}
|
||||
|
||||
export const convertRulesListFiltersToFilterAttributes = (
|
||||
rulesListFilter: RulesListFilters
|
||||
): FilterParameters => {
|
||||
return {
|
||||
actionTypes: rulesListFilter.actionTypes,
|
||||
lastResponse: rulesListFilter.ruleLastRunOutcomes,
|
||||
params: rulesListFilter.ruleParams,
|
||||
search: rulesListFilter.searchText,
|
||||
status: rulesListFilter.ruleStatuses,
|
||||
tags: rulesListFilter.tags,
|
||||
type: rulesListFilter.types,
|
||||
};
|
||||
};
|
||||
|
||||
export const useRulesListFilterStore = ({
|
||||
lastResponseFilter,
|
||||
lastRunOutcomeFilter,
|
||||
rulesListKey = RULES_LIST_FILTERS_KEY,
|
||||
ruleParamFilter,
|
||||
statusFilter,
|
||||
searchFilter,
|
||||
typeFilter,
|
||||
}: FilterStoreProps): {
|
||||
filters: RulesListFilters;
|
||||
setFiltersStore: (params: UpdateFiltersProps) => void;
|
||||
numberOfFiltersStore: number;
|
||||
resetFiltersStore: () => void;
|
||||
} => {
|
||||
const history = useHistory();
|
||||
const urlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
useHashQuery: false,
|
||||
});
|
||||
|
||||
const [rulesListFilterLocal, setRulesListFilterLocal] = useLocalStorage<FilterParameters>(
|
||||
`${RULES_LIST_FILTERS_KEY}_filters`,
|
||||
{}
|
||||
);
|
||||
const hasFilterFromLocalStorage = useMemo(
|
||||
() =>
|
||||
rulesListFilterLocal
|
||||
? !Object.values(rulesListFilterLocal).every((filters) => isEmpty(filters))
|
||||
: false,
|
||||
[rulesListFilterLocal]
|
||||
);
|
||||
|
||||
const rulesListFilterUrl = useMemo(
|
||||
() => urlStateStorage.get<FilterParameters>('_a') ?? {},
|
||||
[urlStateStorage]
|
||||
);
|
||||
|
||||
const hasFilterFromUrl = useMemo(
|
||||
() =>
|
||||
rulesListFilterUrl
|
||||
? !Object.values(rulesListFilterUrl).every((filters) => isEmpty(filters))
|
||||
: false,
|
||||
[rulesListFilterUrl]
|
||||
);
|
||||
|
||||
const filtersStore = useMemo(
|
||||
() =>
|
||||
hasFilterFromUrl ? rulesListFilterUrl : hasFilterFromLocalStorage ? rulesListFilterLocal : {},
|
||||
[hasFilterFromLocalStorage, hasFilterFromUrl, rulesListFilterLocal, rulesListFilterUrl]
|
||||
);
|
||||
const [filters, setFilters] = useState<RulesListFilters>({
|
||||
actionTypes: filtersStore?.actionTypes ?? [],
|
||||
ruleExecutionStatuses: lastResponseFilter ?? [],
|
||||
ruleLastRunOutcomes: filtersStore?.lastResponse ?? lastRunOutcomeFilter ?? [],
|
||||
ruleParams: filtersStore?.params ?? ruleParamFilter ?? {},
|
||||
ruleStatuses: filtersStore?.status ?? statusFilter ?? [],
|
||||
searchText: filtersStore?.search ?? searchFilter ?? '',
|
||||
tags: filtersStore?.tags ?? [],
|
||||
types: filtersStore?.type ?? typeFilter ?? [],
|
||||
kueryNode: undefined,
|
||||
});
|
||||
|
||||
const updateUrlFilters = useCallback(
|
||||
(updatedParams: RulesListFilters) => {
|
||||
urlStateStorage.set('_a', convertRulesListFiltersToFilterAttributes(updatedParams));
|
||||
},
|
||||
[urlStateStorage]
|
||||
);
|
||||
|
||||
const updateLocalFilters = useCallback(
|
||||
(updatedParams: RulesListFilters) => {
|
||||
setRulesListFilterLocal(convertRulesListFiltersToFilterAttributes(updatedParams));
|
||||
},
|
||||
[setRulesListFilterLocal]
|
||||
);
|
||||
|
||||
const setFiltersStore = useCallback(
|
||||
(updateFiltersProps: UpdateFiltersProps) => {
|
||||
const { filter, value } = updateFiltersProps;
|
||||
setFilters((prev) => {
|
||||
const newFilters = {
|
||||
...prev,
|
||||
[filter]: value,
|
||||
};
|
||||
updateUrlFilters(newFilters);
|
||||
updateLocalFilters(newFilters);
|
||||
return newFilters;
|
||||
});
|
||||
},
|
||||
[updateLocalFilters, updateUrlFilters]
|
||||
);
|
||||
|
||||
const resetFiltersStore = useCallback(() => {
|
||||
const resetFilter = {
|
||||
actionTypes: [],
|
||||
ruleExecutionStatuses: [],
|
||||
ruleLastRunOutcomes: [],
|
||||
ruleParams: {},
|
||||
ruleStatuses: [],
|
||||
searchText: '',
|
||||
tags: [],
|
||||
types: [],
|
||||
kueryNode: undefined,
|
||||
};
|
||||
setFilters(resetFilter);
|
||||
updateUrlFilters(resetFilter);
|
||||
updateLocalFilters(resetFilter);
|
||||
}, [updateLocalFilters, updateUrlFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasFilterFromUrl || hasFilterFromLocalStorage) {
|
||||
setFilters({
|
||||
actionTypes: filtersStore?.actionTypes ?? [],
|
||||
ruleExecutionStatuses: lastResponseFilter ?? [],
|
||||
ruleLastRunOutcomes: filtersStore?.lastResponse ?? lastRunOutcomeFilter ?? [],
|
||||
ruleParams: filtersStore?.params ?? ruleParamFilter ?? {},
|
||||
ruleStatuses: filtersStore?.status ?? statusFilter ?? [],
|
||||
searchText: filtersStore?.search ?? searchFilter ?? '',
|
||||
tags: filtersStore?.tags ?? [],
|
||||
types: filtersStore?.type ?? typeFilter ?? [],
|
||||
kueryNode: undefined,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
filters,
|
||||
setFiltersStore,
|
||||
numberOfFiltersStore: Object.values(filters).filter((filter) => !isEmpty(filter)).length,
|
||||
resetFiltersStore,
|
||||
}),
|
||||
[filters, resetFiltersStore, setFiltersStore]
|
||||
);
|
||||
};
|
|
@ -123,6 +123,19 @@ jest.mock('../../../../common/get_experimental_features', () => ({
|
|||
getIsExperimentalFeatureEnabled: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/kibana-utils-plugin/public', () => {
|
||||
const originalModule = jest.requireActual('@kbn/kibana-utils-plugin/public');
|
||||
return {
|
||||
...originalModule,
|
||||
createKbnUrlStateStorage: jest.fn(() => ({
|
||||
get: jest.fn(() => null),
|
||||
set: jest.fn(() => null),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-use/lib/useLocalStorage', () => jest.fn(() => [null, () => null]));
|
||||
|
||||
const ruleTags = ['a', 'b', 'c', 'd'];
|
||||
|
||||
const { loadRuleTypes } = jest.requireMock('../../../lib/rule_api/rule_types');
|
||||
|
|
|
@ -54,7 +54,6 @@ import {
|
|||
Pagination,
|
||||
Percentiles,
|
||||
SnoozeSchedule,
|
||||
RulesListFilters,
|
||||
UpdateFiltersProps,
|
||||
BulkEditActions,
|
||||
UpdateRulesToBulkEditProps,
|
||||
|
@ -107,6 +106,7 @@ import {
|
|||
import { useBulkOperationToast } from '../../../hooks/use_bulk_operation_toast';
|
||||
import { RulesSettingsLink } from '../../../components/rules_setting/rules_settings_link';
|
||||
import { useRulesListUiState as useUiState } from '../../../hooks/use_rules_list_ui_state';
|
||||
import { useRulesListFilterStore } from './hooks/use_rules_list_filter_store';
|
||||
|
||||
// Directly lazy import the flyouts because the suspendedComponentWithProps component
|
||||
// cause a visual hitch due to the loading spinner
|
||||
|
@ -190,23 +190,12 @@ export const RulesList = ({
|
|||
notifications: { toasts },
|
||||
ruleTypeRegistry,
|
||||
} = kibanaServices;
|
||||
|
||||
const canExecuteActions = hasExecuteActionsCapability(capabilities);
|
||||
const [isPerformingAction, setIsPerformingAction] = useState<boolean>(false);
|
||||
const [page, setPage] = useState<Pagination>({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE });
|
||||
const [inputText, setInputText] = useState<string>(searchFilter);
|
||||
|
||||
const [filters, setFilters] = useState<RulesListFilters>({
|
||||
actionTypes: [],
|
||||
ruleExecutionStatuses: lastResponseFilter || [],
|
||||
ruleLastRunOutcomes: lastRunOutcomeFilter || [],
|
||||
ruleParams: ruleParamFilter || {},
|
||||
ruleStatuses: statusFilter || [],
|
||||
searchText: searchFilter || '',
|
||||
tags: [],
|
||||
types: typeFilter || [],
|
||||
kueryNode: undefined,
|
||||
});
|
||||
|
||||
const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState<boolean>(false);
|
||||
const [editFlyoutVisible, setEditFlyoutVisibility] = useState<boolean>(false);
|
||||
const [currentRuleToEdit, setCurrentRuleToEdit] = useState<RuleTableItem | null>(null);
|
||||
|
@ -259,6 +248,17 @@ export const RulesList = ({
|
|||
// Fetch action types
|
||||
const { actionTypes } = useLoadActionTypesQuery();
|
||||
|
||||
const { filters, setFiltersStore, numberOfFiltersStore, resetFiltersStore } =
|
||||
useRulesListFilterStore({
|
||||
lastResponseFilter,
|
||||
lastRunOutcomeFilter,
|
||||
rulesListKey,
|
||||
ruleParamFilter,
|
||||
statusFilter,
|
||||
searchFilter,
|
||||
typeFilter,
|
||||
});
|
||||
|
||||
const rulesTypesFilter = isEmpty(filters.types)
|
||||
? authorizedRuleTypes.map((art) => art.id)
|
||||
: filters.types;
|
||||
|
@ -406,14 +406,10 @@ export const RulesList = ({
|
|||
|
||||
const updateFilters = useCallback(
|
||||
(updateFiltersProps: UpdateFiltersProps) => {
|
||||
const { filter, value } = updateFiltersProps;
|
||||
setFilters((prev) => ({
|
||||
...prev,
|
||||
[filter]: value,
|
||||
}));
|
||||
setFiltersStore(updateFiltersProps);
|
||||
handleUpdateFiltersEffect(updateFiltersProps);
|
||||
},
|
||||
[setFilters, handleUpdateFiltersEffect]
|
||||
[setFiltersStore, handleUpdateFiltersEffect]
|
||||
);
|
||||
|
||||
const handleClearRuleParamFilter = () => updateFilters({ filter: 'ruleParams', value: {} });
|
||||
|
@ -982,6 +978,8 @@ export const RulesList = ({
|
|||
rulesListKey={rulesListKey}
|
||||
config={config}
|
||||
visibleColumns={visibleColumns}
|
||||
numberOfFilters={numberOfFiltersStore}
|
||||
resetFilters={resetFiltersStore}
|
||||
/>
|
||||
{manageLicenseModalOpts && (
|
||||
<ManageLicenseModal
|
||||
|
|
|
@ -92,6 +92,17 @@ jest.mock('../../../lib/rule_api/aggregate_kuery_filter', () => ({
|
|||
loadRuleAggregationsWithKueryFilter: jest.fn(),
|
||||
}));
|
||||
jest.mock('@kbn/alerts-ui-shared', () => ({ MaintenanceWindowCallout: jest.fn(() => <></>) }));
|
||||
jest.mock('@kbn/kibana-utils-plugin/public', () => {
|
||||
const originalModule = jest.requireActual('@kbn/kibana-utils-plugin/public');
|
||||
return {
|
||||
...originalModule,
|
||||
createKbnUrlStateStorage: jest.fn(() => ({
|
||||
get: jest.fn(() => null),
|
||||
set: jest.fn(() => null),
|
||||
})),
|
||||
};
|
||||
});
|
||||
jest.mock('react-use/lib/useLocalStorage', () => jest.fn(() => [null, () => null]));
|
||||
|
||||
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
|
||||
'../../../lib/rule_api/aggregate_kuery_filter'
|
||||
|
|
|
@ -91,6 +91,17 @@ jest.mock('../../../lib/rule_api/aggregate_kuery_filter', () => ({
|
|||
loadRuleAggregationsWithKueryFilter: jest.fn(),
|
||||
}));
|
||||
jest.mock('@kbn/alerts-ui-shared', () => ({ MaintenanceWindowCallout: jest.fn(() => <></>) }));
|
||||
jest.mock('@kbn/kibana-utils-plugin/public', () => {
|
||||
const originalModule = jest.requireActual('@kbn/kibana-utils-plugin/public');
|
||||
return {
|
||||
...originalModule,
|
||||
createKbnUrlStateStorage: jest.fn(() => ({
|
||||
get: jest.fn(() => null),
|
||||
set: jest.fn(() => null),
|
||||
})),
|
||||
};
|
||||
});
|
||||
jest.mock('react-use/lib/useLocalStorage', () => jest.fn(() => [null, () => null]));
|
||||
|
||||
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
|
||||
'../../../lib/rule_api/aggregate_kuery_filter'
|
||||
|
|
|
@ -92,6 +92,17 @@ jest.mock('../../../lib/rule_api/aggregate_kuery_filter', () => ({
|
|||
loadRuleAggregationsWithKueryFilter: jest.fn(),
|
||||
}));
|
||||
jest.mock('@kbn/alerts-ui-shared', () => ({ MaintenanceWindowCallout: jest.fn(() => <></>) }));
|
||||
jest.mock('@kbn/kibana-utils-plugin/public', () => {
|
||||
const originalModule = jest.requireActual('@kbn/kibana-utils-plugin/public');
|
||||
return {
|
||||
...originalModule,
|
||||
createKbnUrlStateStorage: jest.fn(() => ({
|
||||
get: jest.fn(() => null),
|
||||
set: jest.fn(() => null),
|
||||
})),
|
||||
};
|
||||
});
|
||||
jest.mock('react-use/lib/useLocalStorage', () => jest.fn(() => [null, () => null]));
|
||||
|
||||
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
|
||||
'../../../lib/rule_api/aggregate_kuery_filter'
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
CLEAR_SELECTION,
|
||||
TOTAL_RULES,
|
||||
SELECT_ALL_ARIA_LABEL,
|
||||
CLEAR_FILTERS,
|
||||
} from '../translations';
|
||||
import {
|
||||
Rule,
|
||||
|
@ -140,6 +141,8 @@ export interface RulesListTableProps {
|
|||
) => React.ReactNode;
|
||||
renderRuleError?: (rule: RuleTableItem) => React.ReactNode;
|
||||
visibleColumns?: string[];
|
||||
numberOfFilters: number;
|
||||
resetFilters: () => void;
|
||||
}
|
||||
|
||||
interface ConvertRulesToTableItemsOpts {
|
||||
|
@ -205,6 +208,8 @@ export const RulesListTable = (props: RulesListTableProps) => {
|
|||
renderSelectAllDropdown,
|
||||
renderRuleError = EMPTY_RENDER,
|
||||
visibleColumns,
|
||||
resetFilters,
|
||||
numberOfFilters,
|
||||
} = props;
|
||||
|
||||
const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState<number>(-1);
|
||||
|
@ -844,34 +849,56 @@ export const RulesListTable = (props: RulesListTableProps) => {
|
|||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
{numberOfSelectedRules > 0 ? (
|
||||
renderSelectAllDropdown?.()
|
||||
) : (
|
||||
<EuiText
|
||||
size="xs"
|
||||
style={{ fontWeight: euiTheme.font.weight.semiBold }}
|
||||
data-test-subj="totalRulesCount"
|
||||
>
|
||||
{TOTAL_RULES(formattedTotalRules, rulesState.totalItemCount)}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{numberOfSelectedRules > 0 && authorizedToModifyAllRules && (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
aria-label={SELECT_ALL_ARIA_LABEL}
|
||||
data-test-subj="selectAllRulesButton"
|
||||
iconType={isAllSelected ? 'cross' : 'pagesSelect'}
|
||||
onClick={onSelectAll}
|
||||
>
|
||||
{selectAllButtonText}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
{numberOfFilters > 0 && (
|
||||
<EuiFlexItem
|
||||
css={{
|
||||
borderLeft: euiTheme.border.thin,
|
||||
paddingLeft: euiTheme.size.m,
|
||||
}}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
onClick={resetFilters}
|
||||
size="xs"
|
||||
iconSide="left"
|
||||
flush="left"
|
||||
data-test-subj="rules-list-clear-filter"
|
||||
>
|
||||
{CLEAR_FILTERS(numberOfFilters)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{ColumnSelector}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{numberOfSelectedRules > 0 ? (
|
||||
renderSelectAllDropdown?.()
|
||||
) : (
|
||||
<EuiText
|
||||
size="xs"
|
||||
style={{ fontWeight: euiTheme.font.weight.semiBold }}
|
||||
data-test-subj="totalRulesCount"
|
||||
>
|
||||
{TOTAL_RULES(formattedTotalRules, rulesState.totalItemCount)}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{numberOfSelectedRules > 0 && authorizedToModifyAllRules && (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
aria-label={SELECT_ALL_ARIA_LABEL}
|
||||
data-test-subj="selectAllRulesButton"
|
||||
iconType={isAllSelected ? 'cross' : 'pagesSelect'}
|
||||
onClick={onSelectAll}
|
||||
>
|
||||
{selectAllButtonText}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiBasicTable
|
||||
|
|
|
@ -340,6 +340,13 @@ export const CANCEL_BUTTON_TEXT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CLEAR_FILTERS = (numberOfFilters: number) => {
|
||||
return i18n.translate('xpack.triggersActionsUI.sections.rulesList.clearFilterLink', {
|
||||
values: { numberOfFilters },
|
||||
defaultMessage: 'Clear {numberOfFilters, plural, =1 {filter} other {filters}}',
|
||||
});
|
||||
};
|
||||
|
||||
export const getConfirmDeletionModalText = (
|
||||
numIdsToDelete: number,
|
||||
singleTitle: string,
|
||||
|
|
|
@ -27,6 +27,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
async function refreshAlertsList() {
|
||||
const existsClearFilter = await testSubjects.exists('rules-list-clear-filter');
|
||||
const existsRefreshButton = await testSubjects.exists('refreshRulesButton');
|
||||
if (existsClearFilter) {
|
||||
await testSubjects.click('rules-list-clear-filter');
|
||||
} else if (existsRefreshButton) {
|
||||
await testSubjects.click('refreshRulesButton');
|
||||
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
|
||||
}
|
||||
await testSubjects.click('logsTab');
|
||||
await testSubjects.click('rulesTab');
|
||||
}
|
||||
|
|
|
@ -30,6 +30,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
const toasts = getService('toasts');
|
||||
|
||||
async function refreshRulesList() {
|
||||
const existsClearFilter = await testSubjects.exists('rules-list-clear-filter');
|
||||
if (existsClearFilter) {
|
||||
await testSubjects.click('rules-list-clear-filter');
|
||||
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
|
||||
}
|
||||
await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' });
|
||||
await testSubjects.click('manageRulesPageButton');
|
||||
}
|
||||
|
@ -525,6 +530,10 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
expect(filterErrorOnlyResults[0].status).toEqual('Failed');
|
||||
expect(filterErrorOnlyResults[0].duration).toMatch(/\d{2,}:\d{2}/);
|
||||
});
|
||||
|
||||
// Clear it again because it is still selected
|
||||
await refreshRulesList();
|
||||
await assertRulesLength(2);
|
||||
});
|
||||
|
||||
it.skip('should display total rules by status and error banner only when exists rules with status error', async () => {
|
||||
|
@ -673,6 +682,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
expect(filterInventoryRuleOnlyResults[0].interval).toEqual('1 min');
|
||||
expect(filterInventoryRuleOnlyResults[0].duration).toMatch(/\d{2,}:\d{2}/);
|
||||
});
|
||||
|
||||
// Clear it again because it is still selected
|
||||
await testSubjects.click('rules-list-clear-filter');
|
||||
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
|
||||
await assertRulesLength(2);
|
||||
});
|
||||
|
||||
it('should filter rules by the rule status', async () => {
|
||||
|
@ -746,6 +760,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('ruleStatusFilterOption-enabled');
|
||||
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
|
||||
await assertRulesLength(4);
|
||||
|
||||
// Clear it again because it is still selected
|
||||
await testSubjects.click('rules-list-clear-filter');
|
||||
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
|
||||
await assertRulesLength(4);
|
||||
});
|
||||
|
||||
it('should filter rules by the tag', async () => {
|
||||
|
@ -804,6 +823,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('ruleTagFilterOption-c');
|
||||
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
|
||||
await assertRulesLength(2);
|
||||
|
||||
// Clear it again because it is still selected
|
||||
await testSubjects.click('rules-list-clear-filter');
|
||||
await find.waitForDeletedByCssSelector('.euiBasicTable-loading');
|
||||
await assertRulesLength(5);
|
||||
});
|
||||
|
||||
it('should not prevent rules with action execution capabilities from being edited', async () => {
|
||||
|
@ -835,12 +859,11 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
ruleIdList = [rule1.id];
|
||||
|
||||
await refreshRulesList();
|
||||
await assertRulesLength(1);
|
||||
|
||||
await retry.try(async () => {
|
||||
const actionButton = await testSubjects.find('selectActionButton');
|
||||
const disabled = await actionButton.getAttribute('disabled');
|
||||
expect(disabled).toEqual(null);
|
||||
});
|
||||
const actionButton = await testSubjects.find('selectActionButton');
|
||||
const disabled = await actionButton.getAttribute('disabled');
|
||||
expect(disabled).toEqual(null);
|
||||
});
|
||||
|
||||
it('should allow rules to be snoozed using the right side dropdown', async () => {
|
||||
|
@ -851,7 +874,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
ruleIdList = [rule1.id];
|
||||
|
||||
await refreshRulesList();
|
||||
await svlTriggersActionsUI.searchRules(rule1.name);
|
||||
await assertRulesLength(1);
|
||||
|
||||
await testSubjects.click('collapsedItemActions');
|
||||
await testSubjects.click('snoozeButton');
|
||||
|
@ -871,7 +894,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
ruleIdList = [rule1.id];
|
||||
|
||||
await refreshRulesList();
|
||||
await svlTriggersActionsUI.searchRules(rule1.name);
|
||||
await assertRulesLength(1);
|
||||
|
||||
await testSubjects.click('collapsedItemActions');
|
||||
await testSubjects.click('snoozeButton');
|
||||
await testSubjects.click('ruleSnoozeIndefiniteApply');
|
||||
|
@ -895,8 +919,8 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
});
|
||||
|
||||
await refreshRulesList();
|
||||
await assertRulesLength(1);
|
||||
|
||||
await svlTriggersActionsUI.searchRules(rule1.name);
|
||||
await testSubjects.click('collapsedItemActions');
|
||||
await testSubjects.click('snoozeButton');
|
||||
await testSubjects.click('ruleSnoozeCancel');
|
||||
|
@ -910,8 +934,6 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
expect(toastText).toEqual('Rules notification successfully unsnoozed');
|
||||
});
|
||||
|
||||
await svlTriggersActionsUI.searchRules(rule1.name);
|
||||
|
||||
await testSubjects.missingOrFail('rulesListNotifyBadge-snoozed');
|
||||
await testSubjects.missingOrFail('rulesListNotifyBadge-snoozedIndefinitely');
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue