[RAM] [Flapping] Allow rules settings to be access when users have no rules (#149656)

## Summary
Resolves: https://github.com/elastic/kibana/issues/149366

Move the rules settings link to the page header area so users can access
the rules settings even when they have no rules. Also make the rules
settings available on the logs page.

This change means we no longer have a purpose for the
`showCreateRuleButton` prop since we're no longer showing the "Create
Rule" button alongside the rule filters. So that prop has been removed.

This PR also adds small enhancements to our queries to no longer fetch
on windows focus as it was creating a slight flicker of the spinner when
the user has no rules. Refetching on focus is a little too aggressive
anyways since we already have a timed refetcher.

## Rules page (no rules)

![move_settings](https://user-images.githubusercontent.com/74562234/214997907-034f32fb-f9c6-4b90-8d60-7cc1746b1329.png)

## Rules page (with rules)

![withrule](https://user-images.githubusercontent.com/74562234/214997956-05de257e-41e2-423c-9f95-5928632b8dda.png)

## Logs page

![movesettingslogs](https://user-images.githubusercontent.com/74562234/214997928-5b1702ac-572b-4953-a95b-2e4143f3f17e.png)

### Checklist
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Jiawei Wu 2023-01-30 14:00:12 -08:00 committed by GitHub
parent 7608dfb023
commit 987cb6be76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 41 additions and 76 deletions

View file

@ -103,7 +103,6 @@ function RulesPage() {
<RuleList <RuleList
filteredRuleTypes={filteredRuleTypes} filteredRuleTypes={filteredRuleTypes}
showActionFilter={false} showActionFilter={false}
showCreateRuleButton={false}
ruleDetailsRoute="alerts/rules/:ruleId" ruleDetailsRoute="alerts/rules/:ruleId"
statusFilter={status} statusFilter={status}
onStatusFilterChange={setStatus} onStatusFilterChange={setStatus}

View file

@ -7,8 +7,7 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { EuiButton, EuiButtonEmpty, EuiPageTemplate } from '@elastic/eui'; import { EuiButton, EuiPageTemplate } from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
export const EmptyPrompt = ({ export const EmptyPrompt = ({
onCreateRulesClick, onCreateRulesClick,
@ -17,7 +16,6 @@ export const EmptyPrompt = ({
onCreateRulesClick: () => void; onCreateRulesClick: () => void;
showCreateRule: boolean; showCreateRule: boolean;
}) => { }) => {
const { docLinks } = useKibana().services;
const renderActions = () => { const renderActions = () => {
if (showCreateRule) { if (showCreateRule) {
return [ return [
@ -33,17 +31,6 @@ export const EmptyPrompt = ({
defaultMessage="Create rule" defaultMessage="Create rule"
/> />
</EuiButton>, </EuiButton>,
<EuiButtonEmpty
href={docLinks.links.alerting.guide}
target="_blank"
iconType="help"
data-test-subj="documentationLink"
>
<FormattedMessage
id="xpack.triggersActionsUI.home.docsLinkText"
defaultMessage="Documentation"
/>
</EuiButtonEmpty>,
]; ];
} }
return null; return null;

View file

@ -76,7 +76,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
RulesList, RulesList,
'xl' 'xl'
)({ )({
showCreateRuleButton: false,
showCreateRuleButtonInPrompt: true, showCreateRuleButtonInPrompt: true,
setHeaderActions, setHeaderActions,
}); });

View file

@ -35,6 +35,7 @@ export const useLoadActionTypesQuery = () => {
queryKey: ['loadActionTypes'], queryKey: ['loadActionTypes'],
queryFn, queryFn,
onError: onErrorFn, onError: onErrorFn,
refetchOnWindowFocus: false,
}); });
const sortedResult = data const sortedResult = data

View file

@ -16,6 +16,7 @@ export const useLoadConfigQuery = () => {
return triggersActionsUiConfig({ http }); return triggersActionsUiConfig({ http });
}, },
initialData: { isUsingSecurity: false }, initialData: { isUsingSecurity: false },
refetchOnWindowFocus: false,
}); });
return { return {

View file

@ -79,6 +79,7 @@ export const useLoadRuleAggregationsQuery = (props: UseLoadRuleAggregationsQuery
enabled, enabled,
keepPreviousData: true, keepPreviousData: true,
cacheTime: 0, cacheTime: 0,
refetchOnWindowFocus: false,
}); });
const aggregation = data const aggregation = data

View file

@ -54,6 +54,7 @@ export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
queryKey: ['loadRuleTypes'], queryKey: ['loadRuleTypes'],
queryFn, queryFn,
onError: onErrorFn, onError: onErrorFn,
refetchOnWindowFocus: false,
}); });
const filteredIndex = data ? getFilteredIndex(data, filteredRuleTypes) : new Map(); const filteredIndex = data ? getFilteredIndex(data, filteredRuleTypes) : new Map();

View file

@ -79,6 +79,7 @@ export const useLoadRulesQuery = (props: UseLoadRulesQueryProps) => {
enabled, enabled,
keepPreviousData: true, keepPreviousData: true,
cacheTime: 0, cacheTime: 0,
refetchOnWindowFocus: false,
}); });
const hasData = Boolean(rulesResponse && rulesResponse.data.length > 0); const hasData = Boolean(rulesResponse && rulesResponse.data.length > 0);

View file

@ -45,6 +45,7 @@ export function useLoadTagsQuery(props: UseLoadTagsQueryProps) {
queryFn, queryFn,
onError: onErrorFn, onError: onErrorFn,
enabled, enabled,
refetchOnWindowFocus: false,
}); });
return { return {

View file

@ -50,8 +50,6 @@ export const useRulesListUiState = ({
showRulesList: boolean; showRulesList: boolean;
showNoAuthPrompt: boolean; showNoAuthPrompt: boolean;
showCreateFirstRulePrompt: boolean; showCreateFirstRulePrompt: boolean;
showHeaderWithoutCreateButton: boolean;
showHeaderWithCreateButton: boolean;
} => { } => {
const hasEmptyTypesFilter = hasDefaultRuleTypesFiltersOn ? true : isEmpty(filters.types); const hasEmptyTypesFilter = hasDefaultRuleTypesFiltersOn ? true : isEmpty(filters.types);
const isFilterApplied = getFilterApplied({ hasEmptyTypesFilter, filters }); const isFilterApplied = getFilterApplied({ hasEmptyTypesFilter, filters });
@ -63,15 +61,11 @@ export const useRulesListUiState = ({
const showSpinner = const showSpinner =
isInitialLoading && (isLoadingRuleTypes || (!showNoAuthPrompt && isLoadingRules)); isInitialLoading && (isLoadingRuleTypes || (!showNoAuthPrompt && isLoadingRules));
const showRulesList = !showSpinner && !showCreateFirstRulePrompt && !showNoAuthPrompt; const showRulesList = !showSpinner && !showCreateFirstRulePrompt && !showNoAuthPrompt;
const showHeaderWithCreateButton = showRulesList && authorizedToCreateAnyRules;
const showHeaderWithoutCreateButton = showRulesList && !authorizedToCreateAnyRules;
return { return {
showSpinner, showSpinner,
showRulesList, showRulesList,
showNoAuthPrompt, showNoAuthPrompt,
showCreateFirstRulePrompt, showCreateFirstRulePrompt,
showHeaderWithoutCreateButton,
showHeaderWithCreateButton,
}; };
}; };

View file

@ -51,6 +51,7 @@ import {
withBulkRuleOperations, withBulkRuleOperations,
} from '../../common/components/with_bulk_rule_api_operations'; } from '../../common/components/with_bulk_rule_api_operations';
import { useMultipleSpaces } from '../../../hooks/use_multiple_spaces'; import { useMultipleSpaces } from '../../../hooks/use_multiple_spaces';
import { RulesSettingsLink } from '../../../components/rules_setting/rules_settings_link';
const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>; const getEmptyFunctionComponent: React.FC<SpacesContextProps> = ({ children }) => <>{children}</>;
@ -206,7 +207,7 @@ export const RuleEventLogListTable = <T extends RuleEventLogListOptions>(
}, [sortingColumns]); }, [sortingColumns]);
useEffect(() => { useEffect(() => {
setHeaderActions?.([<RulesListDocLink />]); setHeaderActions?.([<RulesSettingsLink />, <RulesListDocLink />]);
return () => setHeaderActions?.(); return () => setHeaderActions?.();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View file

@ -38,12 +38,6 @@ export default {
type: 'boolean', type: 'boolean',
}, },
}, },
showCreateRuleButton: {
defaultValue: true,
control: {
type: 'boolean',
},
},
ruleDetailsRoute: { ruleDetailsRoute: {
control: { control: {
type: 'text', type: 'text',

View file

@ -27,6 +27,9 @@ import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experime
import { useKibana } from '../../../../common/lib/kibana'; import { useKibana } from '../../../../common/lib/kibana';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { IToasts } from '@kbn/core/public'; import { IToasts } from '@kbn/core/public';
import { CreateRuleButton } from './create_rule_button';
import { RulesListDocLink } from './rules_list_doc_link';
import { RulesSettingsLink } from '../../../components/rules_setting/rules_settings_link';
import { import {
mockedRulesData, mockedRulesData,
@ -397,17 +400,33 @@ describe('rules_list ', () => {
}); });
}); });
describe('showCreateRuleButton prop', () => { describe('setHeaderActions', () => {
it('hides the Create Rule button', async () => { it('should not render the Create Rule button', async () => {
renderWithProviders(<RulesList showCreateRuleButton={false} />); renderWithProviders(<RulesList />);
await waitForElementToBeRemoved(() => screen.queryByTestId('centerJustifiedSpinner')); await waitForElementToBeRemoved(() => screen.queryByTestId('centerJustifiedSpinner'));
expect(screen.queryAllByTestId('createRuleButton')).toHaveLength(0); expect(screen.queryAllByTestId('createRuleButton')).toHaveLength(0);
}); });
it('shows the Create Rule button by default', async () => { it('should set header actions correctly when the user is authorized to create rules', async () => {
renderWithProviders(<RulesList />); const setHeaderActionsMock = jest.fn();
expect(await screen.findAllByTestId('createRuleButton')).toHaveLength(1); renderWithProviders(<RulesList setHeaderActions={setHeaderActionsMock} />);
await waitForElementToBeRemoved(() => screen.queryByTestId('centerJustifiedSpinner'));
expect(setHeaderActionsMock.mock.lastCall[0][0].type).toEqual(CreateRuleButton);
expect(setHeaderActionsMock.mock.lastCall[0][1].type).toEqual(RulesSettingsLink);
expect(setHeaderActionsMock.mock.lastCall[0][2].type).toEqual(RulesListDocLink);
});
it('should set header actions correctly when the user is not authorized to creat rules', async () => {
loadRuleTypes.mockResolvedValueOnce([]);
const setHeaderActionsMock = jest.fn();
renderWithProviders(<RulesList setHeaderActions={setHeaderActionsMock} />);
await waitForElementToBeRemoved(() => screen.queryByTestId('centerJustifiedSpinner'));
// Do not render the create rule button since the user is not authorized
expect(setHeaderActionsMock.mock.lastCall[0][0].type).toEqual(RulesSettingsLink);
expect(setHeaderActionsMock.mock.lastCall[0][1].type).toEqual(RulesListDocLink);
}); });
}); });

View file

@ -114,7 +114,6 @@ export interface RulesListProps {
filteredRuleTypes?: string[]; filteredRuleTypes?: string[];
showActionFilter?: boolean; showActionFilter?: boolean;
ruleDetailsRoute?: string; ruleDetailsRoute?: string;
showCreateRuleButton?: boolean;
showCreateRuleButtonInPrompt?: boolean; showCreateRuleButtonInPrompt?: boolean;
setHeaderActions?: (components?: React.ReactNode[]) => void; setHeaderActions?: (components?: React.ReactNode[]) => void;
statusFilter?: RuleStatus[]; statusFilter?: RuleStatus[];
@ -146,7 +145,6 @@ export const RulesList = ({
filteredRuleTypes = EMPTY_ARRAY, filteredRuleTypes = EMPTY_ARRAY,
showActionFilter = true, showActionFilter = true,
ruleDetailsRoute, ruleDetailsRoute,
showCreateRuleButton = true,
showCreateRuleButtonInPrompt = false, showCreateRuleButtonInPrompt = false,
statusFilter, statusFilter,
onStatusFilterChange, onStatusFilterChange,
@ -271,14 +269,7 @@ export const RulesList = ({
refresh, refresh,
}); });
const { const { showSpinner, showRulesList, showNoAuthPrompt, showCreateFirstRulePrompt } = useUiState({
showSpinner,
showRulesList,
showNoAuthPrompt,
showCreateFirstRulePrompt,
showHeaderWithCreateButton,
showHeaderWithoutCreateButton,
} = useUiState({
authorizedToCreateAnyRules, authorizedToCreateAnyRules,
filters, filters,
hasDefaultRuleTypesFiltersOn, hasDefaultRuleTypesFiltersOn,
@ -612,22 +603,12 @@ export const RulesList = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!setHeaderActions) return; setHeaderActions?.([
...(authorizedToCreateAnyRules ? [<CreateRuleButton openFlyout={openFlyout} />] : []),
if (showHeaderWithoutCreateButton) { <RulesSettingsLink />,
setHeaderActions([<RulesListDocLink />, <RulesSettingsLink />]); <RulesListDocLink />,
return; ]);
} }, [authorizedToCreateAnyRules]);
if (showHeaderWithCreateButton) {
setHeaderActions([
<CreateRuleButton openFlyout={openFlyout} />,
<RulesSettingsLink />,
<RulesListDocLink />,
]);
return;
}
setHeaderActions();
}, [showHeaderWithCreateButton, showHeaderWithoutCreateButton]);
useEffect(() => { useEffect(() => {
return () => setHeaderActions?.(); return () => setHeaderActions?.();
@ -789,11 +770,8 @@ export const RulesList = ({
tags={tags} tags={tags}
filterOptions={filterOptions} filterOptions={filterOptions}
actionTypes={actionTypes} actionTypes={actionTypes}
authorizedToCreateAnyRules={authorizedToCreateAnyRules}
showCreateRuleButton={showCreateRuleButton}
lastUpdate={lastUpdate} lastUpdate={lastUpdate}
showErrors={showErrors} showErrors={showErrors}
openFlyout={openFlyout}
updateFilters={updateFilters} updateFilters={updateFilters}
setInputText={setInputText} setInputText={setInputText}
onClearSelection={onClearSelection} onClearSelection={onClearSelection}

View file

@ -28,7 +28,6 @@ import { TypeFilter, TypeFilterProps } from './type_filter';
import { ActionTypeFilter } from './action_type_filter'; import { ActionTypeFilter } from './action_type_filter';
import { RuleTagFilter } from './rule_tag_filter'; import { RuleTagFilter } from './rule_tag_filter';
import { RuleStatusFilter } from './rule_status_filter'; import { RuleStatusFilter } from './rule_status_filter';
import { CreateRuleButton } from './create_rule_button';
const ENTER_KEY = 13; const ENTER_KEY = 13;
@ -41,11 +40,8 @@ interface RulesListFiltersBarProps {
tags: string[]; tags: string[];
filterOptions: TypeFilterProps['options']; filterOptions: TypeFilterProps['options'];
actionTypes: ActionType[]; actionTypes: ActionType[];
authorizedToCreateAnyRules: boolean;
showCreateRuleButton: boolean;
lastUpdate: string; lastUpdate: string;
showErrors: boolean; showErrors: boolean;
openFlyout: () => void;
updateFilters: (updateFiltersProps: UpdateFiltersProps) => void; updateFilters: (updateFiltersProps: UpdateFiltersProps) => void;
setInputText: (text: string) => void; setInputText: (text: string) => void;
onClearSelection: () => void; onClearSelection: () => void;
@ -63,11 +59,8 @@ export const RulesListFiltersBar = React.memo((props: RulesListFiltersBarProps)
tags, tags,
actionTypes, actionTypes,
filterOptions, filterOptions,
authorizedToCreateAnyRules,
showCreateRuleButton,
lastUpdate, lastUpdate,
showErrors, showErrors,
openFlyout,
updateFilters, updateFilters,
setInputText, setInputText,
onClearSelection, onClearSelection,
@ -155,11 +148,6 @@ export const RulesListFiltersBar = React.memo((props: RulesListFiltersBarProps)
} }
/> />
<EuiFlexGroup gutterSize="s"> <EuiFlexGroup gutterSize="s">
{authorizedToCreateAnyRules && showCreateRuleButton ? (
<EuiFlexItem grow={false}>
<CreateRuleButton openFlyout={openFlyout} />
</EuiFlexItem>
) : null}
<EuiFlexItem> <EuiFlexItem>
<EuiFieldSearch <EuiFieldSearch
fullWidth fullWidth