mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[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)  ## Rules page (with rules)  ## Logs page  ### 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:
parent
7608dfb023
commit
987cb6be76
15 changed files with 41 additions and 76 deletions
|
@ -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}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -76,7 +76,6 @@ export const TriggersActionsUIHome: React.FunctionComponent<RouteComponentProps<
|
||||||
RulesList,
|
RulesList,
|
||||||
'xl'
|
'xl'
|
||||||
)({
|
)({
|
||||||
showCreateRuleButton: false,
|
|
||||||
showCreateRuleButtonInPrompt: true,
|
showCreateRuleButtonInPrompt: true,
|
||||||
setHeaderActions,
|
setHeaderActions,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const useLoadConfigQuery = () => {
|
||||||
return triggersActionsUiConfig({ http });
|
return triggersActionsUiConfig({ http });
|
||||||
},
|
},
|
||||||
initialData: { isUsingSecurity: false },
|
initialData: { isUsingSecurity: false },
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -45,6 +45,7 @@ export function useLoadTagsQuery(props: UseLoadTagsQueryProps) {
|
||||||
queryFn,
|
queryFn,
|
||||||
onError: onErrorFn,
|
onError: onErrorFn,
|
||||||
enabled,
|
enabled,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -38,12 +38,6 @@ export default {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
showCreateRuleButton: {
|
|
||||||
defaultValue: true,
|
|
||||||
control: {
|
|
||||||
type: 'boolean',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ruleDetailsRoute: {
|
ruleDetailsRoute: {
|
||||||
control: {
|
control: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
|
||||||
setHeaderActions([<RulesListDocLink />, <RulesSettingsLink />]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (showHeaderWithCreateButton) {
|
|
||||||
setHeaderActions([
|
|
||||||
<CreateRuleButton openFlyout={openFlyout} />,
|
|
||||||
<RulesSettingsLink />,
|
<RulesSettingsLink />,
|
||||||
<RulesListDocLink />,
|
<RulesListDocLink />,
|
||||||
]);
|
]);
|
||||||
return;
|
}, [authorizedToCreateAnyRules]);
|
||||||
}
|
|
||||||
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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue