mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ResponseOps][Rules]Hide rule actions instead of disabling them in the rules list (#216783)
Closes https://github.com/elastic/kibana/issues/210979 ## Summary - This PR updates the rules list and rules details pages to hide actions for the users with read-only access. Previously, these actions were disabled, but now they are completely hidden. Specifically: - on the `Rules List` page, the snooze bell icon and the table row actions are now hidden for the users with read-only access - on the `Rule Details` page, the actions button is now hidden (previously, the users could click on it, but the options were disabled) <img width="1899" alt="Screenshot 2025-04-02 at 14 06 53" src="https://github.com/user-attachments/assets/712297bf-b807-4ecc-87da-a32cd67d169f" /> <img width="1899" alt="Screenshot 2025-04-02 at 14 07 06" src="https://github.com/user-attachments/assets/a88762fa-feeb-4117-9dc4-31744c752d82" />
This commit is contained in:
parent
429a9db67d
commit
afc5274fb8
10 changed files with 152 additions and 129 deletions
|
@ -53,7 +53,6 @@ describe('rule_actions_popover', () => {
|
|||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
onRunRule={onRunRuleMock}
|
||||
/>
|
||||
|
@ -77,7 +76,6 @@ describe('rule_actions_popover', () => {
|
|||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
onRunRule={onRunRuleMock}
|
||||
/>
|
||||
|
@ -106,7 +104,6 @@ describe('rule_actions_popover', () => {
|
|||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
onRunRule={onRunRuleMock}
|
||||
/>
|
||||
|
@ -134,7 +131,6 @@ describe('rule_actions_popover', () => {
|
|||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
onRunRule={onRunRuleMock}
|
||||
/>
|
||||
|
@ -163,7 +159,6 @@ describe('rule_actions_popover', () => {
|
|||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
onRunRule={onRunRuleMock}
|
||||
/>
|
||||
|
@ -192,7 +187,6 @@ describe('rule_actions_popover', () => {
|
|||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
onRunRule={onRunRuleMock}
|
||||
/>
|
||||
|
@ -212,29 +206,4 @@ describe('rule_actions_popover', () => {
|
|||
expect(screen.queryByText('Run rule')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables buttons when the user does not have enough permission', async () => {
|
||||
const rule = mockRule();
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={false}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
onRunRule={onRunRuleMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const actionButton = screen.getByTestId('ruleActionsButton');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText('Delete rule').closest('button')).toBeDisabled();
|
||||
expect(screen.getByText('Update API key').closest('button')).toBeDisabled();
|
||||
expect(screen.getByText('Disable').closest('button')).toBeDisabled();
|
||||
expect(screen.getByText('Run rule').closest('button')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,7 +13,6 @@ import { Rule } from '../../../..';
|
|||
|
||||
export interface RuleActionsPopoverProps {
|
||||
rule: Rule;
|
||||
canSaveRule: boolean;
|
||||
onDelete: (ruleId: string) => void;
|
||||
onApiKeyUpdate: (ruleId: string) => void;
|
||||
onEnableDisable: (enable: boolean) => void;
|
||||
|
@ -22,7 +21,6 @@ export interface RuleActionsPopoverProps {
|
|||
|
||||
export const RuleActionsPopover: React.FunctionComponent<RuleActionsPopoverProps> = ({
|
||||
rule,
|
||||
canSaveRule,
|
||||
onDelete,
|
||||
onApiKeyUpdate,
|
||||
onEnableDisable,
|
||||
|
@ -63,7 +61,6 @@ export const RuleActionsPopover: React.FunctionComponent<RuleActionsPopoverProps
|
|||
id: 0,
|
||||
items: [
|
||||
{
|
||||
disabled: !canSaveRule,
|
||||
'data-test-subj': 'disableButton',
|
||||
onClick: async () => {
|
||||
setIsPopoverOpen(false);
|
||||
|
@ -80,7 +77,6 @@ export const RuleActionsPopover: React.FunctionComponent<RuleActionsPopoverProps
|
|||
),
|
||||
},
|
||||
{
|
||||
disabled: !canSaveRule,
|
||||
'data-test-subj': 'updateAPIKeyButton',
|
||||
onClick: () => {
|
||||
setIsPopoverOpen(false);
|
||||
|
@ -92,7 +88,6 @@ export const RuleActionsPopover: React.FunctionComponent<RuleActionsPopoverProps
|
|||
),
|
||||
},
|
||||
{
|
||||
disabled: !canSaveRule,
|
||||
'data-test-subj': 'runRuleButton',
|
||||
onClick: () => {
|
||||
setIsPopoverOpen(false);
|
||||
|
@ -104,7 +99,6 @@ export const RuleActionsPopover: React.FunctionComponent<RuleActionsPopoverProps
|
|||
),
|
||||
},
|
||||
{
|
||||
disabled: !canSaveRule,
|
||||
className: 'ruleActionsPopover__deleteButton',
|
||||
'data-test-subj': 'deleteRuleButton',
|
||||
onClick: () => {
|
||||
|
|
|
@ -143,6 +143,32 @@ describe('rule_details', () => {
|
|||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('does not render actions button if the user has only read permissions', async () => {
|
||||
const rule = mockRule();
|
||||
const mockedRuleType: RuleType = {
|
||||
id: '.noop',
|
||||
name: 'No Op',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
recoveryActionGroup,
|
||||
actionVariables: { context: [], state: [], params: [] },
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
producer: ALERTING_FEATURE_ID,
|
||||
authorizedConsumers: {
|
||||
ALERTING_FEATURE_ID: { read: true, all: false },
|
||||
},
|
||||
enabledInLicense: true,
|
||||
category: 'my-category',
|
||||
isExportable: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<RuleDetails rule={rule} ruleType={mockedRuleType} actionTypes={[]} {...mockRuleApis} />
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="ruleActionsButton"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('renders the rule error banner with error message, when rule has a license error', () => {
|
||||
const rule = mockRule({
|
||||
enabled: true,
|
||||
|
|
|
@ -418,19 +418,20 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
</EuiFlexGroup>
|
||||
}
|
||||
rightSideItems={[
|
||||
<RuleActionsPopover
|
||||
canSaveRule={canSaveRule}
|
||||
rule={rule}
|
||||
onDelete={(ruleId) => {
|
||||
setIsDeleteModalVisibility(true);
|
||||
setRulesToDelete([ruleId]);
|
||||
}}
|
||||
onApiKeyUpdate={(ruleId) => {
|
||||
setRulesToUpdateAPIKey([ruleId]);
|
||||
}}
|
||||
onEnableDisable={onEnableDisable}
|
||||
onRunRule={onRunRule}
|
||||
/>,
|
||||
canSaveRule && (
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={(ruleId) => {
|
||||
setIsDeleteModalVisibility(true);
|
||||
setRulesToDelete([ruleId]);
|
||||
}}
|
||||
onApiKeyUpdate={(ruleId) => {
|
||||
setRulesToUpdateAPIKey([ruleId]);
|
||||
}}
|
||||
onEnableDisable={onEnableDisable}
|
||||
onRunRule={onRunRule}
|
||||
/>
|
||||
),
|
||||
editButton,
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="refreshRulesButton"
|
||||
|
|
|
@ -147,7 +147,7 @@ describe('CollapsedItemActions', () => {
|
|||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('renders panel items as disabled', async () => {
|
||||
test('does not render panel items when rule is not editable', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<CollapsedItemActions {...getPropsWithRule({ isEditable: false })} />
|
||||
);
|
||||
|
@ -155,9 +155,8 @@ describe('CollapsedItemActions', () => {
|
|||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled
|
||||
).toBeTruthy();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="selectActionButton"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('renders closed popover initially and opens on click with all actions enabled', async () => {
|
||||
|
@ -311,21 +310,6 @@ describe('CollapsedItemActions', () => {
|
|||
expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule');
|
||||
});
|
||||
|
||||
test('renders actions correctly when rule is not editable', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<CollapsedItemActions {...getPropsWithRule({ isEditable: false })} />
|
||||
);
|
||||
wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click');
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.find(`[data-test-subj="selectActionButton"] button`).prop('disabled')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('renders actions correctly when rule is not enabled due to license', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<CollapsedItemActions {...getPropsWithRule({ enabledInLicense: false })} />
|
||||
|
|
|
@ -129,25 +129,6 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
|
|||
? !ruleTypeRegistry.get(item.ruleTypeId).requiresAppContext
|
||||
: false;
|
||||
|
||||
const button = (
|
||||
<EuiButtonIcon
|
||||
disabled={!item.isEditable}
|
||||
data-test-subj="selectActionButton"
|
||||
data-testid="selectActionButton"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Actions for "{name}" column',
|
||||
values: {
|
||||
name: item.name,
|
||||
},
|
||||
}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
const isSnoozed = useMemo(() => {
|
||||
return isRuleSnoozed(item);
|
||||
}, [item]);
|
||||
|
@ -351,28 +332,46 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
|
|||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
data-test-subj="collapsedItemActions"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={panels}
|
||||
className="actCollapsedItemActions"
|
||||
data-test-subj="collapsedActionPanel"
|
||||
data-testid="collapsedActionPanel"
|
||||
css={collapsedItemActionsCss}
|
||||
/>
|
||||
</EuiPopover>
|
||||
{isUntrackAlertsModalOpen && (
|
||||
<UntrackAlertsModal onCancel={onDisableModalClose} onConfirm={onDisable} />
|
||||
)}
|
||||
</>
|
||||
item.isEditable && (
|
||||
<>
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
data-test-subj="selectActionButton"
|
||||
data-testid="selectActionButton"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActons.popoverButtonTitle',
|
||||
{
|
||||
defaultMessage: 'Actions for "{name}" column',
|
||||
values: {
|
||||
name: item.name,
|
||||
},
|
||||
}
|
||||
)}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
data-test-subj="collapsedItemActions"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={panels}
|
||||
className="actCollapsedItemActions"
|
||||
data-test-subj="collapsedActionPanel"
|
||||
data-testid="collapsedActionPanel"
|
||||
css={collapsedItemActionsCss}
|
||||
/>
|
||||
</EuiPopover>
|
||||
{isUntrackAlertsModalOpen && (
|
||||
<UntrackAlertsModal onCancel={onDisableModalClose} onConfirm={onDisable} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -113,6 +113,23 @@ describe('RulesListNotifyBadge', () => {
|
|||
expect(onRuleChanged).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not allow the user to snooze the rule unless they have edit permissions', async () => {
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
muteAll: false,
|
||||
isSnoozedUntil: null,
|
||||
}}
|
||||
onRuleChanged={onRuleChanged}
|
||||
snoozeRule={snoozeRule}
|
||||
unsnoozeRule={unsnoozeRule}
|
||||
isRuleEditable={false}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId('rulesListNotifyBadge')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow the user to unsnooze rules', async () => {
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
|
@ -181,6 +198,26 @@ describe('RulesListNotifyBadge', () => {
|
|||
expect(screen.getByTestId('rulesListNotifyBadge-invalidSnooze')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the existing snooze schedule as disabled when the user does not have edit permissions', async () => {
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
snoozeSettings={{
|
||||
name: 'rule 1',
|
||||
muteAll: false,
|
||||
isSnoozedUntil: moment('1990-02-01').toDate(),
|
||||
}}
|
||||
disabled={true}
|
||||
onRuleChanged={onRuleChanged}
|
||||
snoozeRule={snoozeRule}
|
||||
unsnoozeRule={unsnoozeRule}
|
||||
isRuleEditable={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('rulesListNotifyBadge-snoozed')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('rulesListNotifyBadge-snoozed')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should clear an infinitive snooze schedule', async () => {
|
||||
render(
|
||||
<RulesListNotifyBadge
|
||||
|
|
|
@ -73,11 +73,13 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
unsnoozeRule,
|
||||
showOnHover = false,
|
||||
showTooltipInline = false,
|
||||
isRuleEditable = true,
|
||||
}) => {
|
||||
const [requestInFlight, setRequestInFlightLoading] = useState(false);
|
||||
const isLoading = loading || requestInFlight;
|
||||
const isDisabled = Boolean(disabled) || !snoozeSettings;
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
|
||||
const togglePopover = useCallback(() => {
|
||||
setIsPopoverOpen((prev) => {
|
||||
const newState = !prev;
|
||||
|
@ -303,6 +305,9 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
}, [isLoading, isDisabled, snoozeButtonAriaLabel, togglePopover]);
|
||||
|
||||
const button = useMemo(() => {
|
||||
if (!isScheduled && !isSnoozed && !isSnoozedIndefinitely && !isRuleEditable) {
|
||||
return null;
|
||||
}
|
||||
if (!isSnoozeValid) {
|
||||
return (
|
||||
<InvalidSnoozeButton
|
||||
|
@ -337,14 +342,17 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
scheduledSnoozeButton,
|
||||
indefiniteSnoozeButton,
|
||||
snoozedButton,
|
||||
isRuleEditable,
|
||||
]);
|
||||
|
||||
const buttonWithToolTip = useMemo(() => {
|
||||
if (!isSnoozeValid) {
|
||||
return (
|
||||
<EuiToolTip title={INVALID_SNOOZE_TOOLTIP_TITLE} content={INVALID_SNOOZE_TOOLTIP_CONTENT}>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
button && (
|
||||
<EuiToolTip title={INVALID_SNOOZE_TOOLTIP_TITLE} content={INVALID_SNOOZE_TOOLTIP_CONTENT}>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -356,21 +364,23 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
: snoozeTimeLeft;
|
||||
|
||||
return (
|
||||
<EuiToolTip
|
||||
title={
|
||||
tooltipContent
|
||||
? i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.timeRemaining',
|
||||
{
|
||||
defaultMessage: 'Time remaining',
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
content={tooltipContent}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
button && (
|
||||
<EuiToolTip
|
||||
title={
|
||||
tooltipContent
|
||||
? i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.rulesListNotifyBadge.timeRemaining',
|
||||
{
|
||||
defaultMessage: 'Time remaining',
|
||||
}
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
content={tooltipContent}
|
||||
>
|
||||
{button}
|
||||
</EuiToolTip>
|
||||
)
|
||||
);
|
||||
}, [isSnoozeValid, disabled, isPopoverOpen, showTooltipInline, snoozeTimeLeft, button]);
|
||||
|
||||
|
@ -392,7 +402,7 @@ export const RulesListNotifyBadge: React.FunctionComponent<RulesListNotifyBadgeP
|
|||
[closePopover, snoozeRule, onRuleChanged, toasts]
|
||||
);
|
||||
|
||||
const popover = (
|
||||
const popover = buttonWithToolTip && (
|
||||
<EuiPopover
|
||||
data-test-subj="rulesListNotifyBadge"
|
||||
isOpen={isPopoverOpen && !isDisabled}
|
||||
|
|
|
@ -26,6 +26,7 @@ export interface RulesListNotifyBadgeProps {
|
|||
unsnoozeRule: (scheduleIds?: string[]) => Promise<void>;
|
||||
showTooltipInline?: boolean;
|
||||
showOnHover?: boolean;
|
||||
isRuleEditable?: boolean;
|
||||
}
|
||||
|
||||
export type RulesListNotifyBadgePropsWithApi = Pick<
|
||||
|
|
|
@ -539,6 +539,7 @@ export const RulesListTable = (props: RulesListTableProps) => {
|
|||
if (!rule.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RulesListNotifyBadge
|
||||
showOnHover
|
||||
|
@ -550,6 +551,7 @@ export const RulesListTable = (props: RulesListTableProps) => {
|
|||
await onSnoozeRule(rule, snoozeSchedule);
|
||||
}}
|
||||
unsnoozeRule={async (scheduleIds) => await onUnsnoozeRule(rule, scheduleIds)}
|
||||
isRuleEditable={rule.isEditable}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue