Alerting: Hide edit/view rule buttons according to deleting/creating state (#90375)

This commit is contained in:
Tom Ratcliffe 2024-07-19 10:55:12 +01:00 committed by GitHub
parent dbc755925d
commit 7829fced94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 85 additions and 34 deletions

View File

@ -1,13 +1,8 @@
import { render } from '@testing-library/react'; import { render, userEvent, screen } from 'test/test-utils';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { byRole } from 'testing-library-selector'; import { byRole } from 'testing-library-selector';
import { setPluginExtensionsHook } from '@grafana/runtime'; import { setPluginExtensionsHook } from '@grafana/runtime';
import { mockApi, setupMswServer } from 'app/features/alerting/unified/mockApi'; import { mockApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
import { configureStore } from 'app/store/configureStore';
import { CombinedRule } from 'app/types/unified-alerting';
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities'; import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
import { getCloudRule, getGrafanaRule, getMockPluginMeta } from '../../mocks'; import { getCloudRule, getGrafanaRule, getMockPluginMeta } from '../../mocks';
@ -36,18 +31,6 @@ const ui = {
}, },
}; };
function renderRulesTable(rule: CombinedRule) {
const store = configureStore();
render(
<Provider store={store}>
<MemoryRouter>
<RulesTable rules={[rule]} />
</MemoryRouter>
</Provider>
);
}
const user = userEvent.setup(); const user = userEvent.setup();
const server = setupMswServer(); const server = setupMswServer();
@ -57,6 +40,7 @@ describe('RulesTable RBAC', () => {
...getMockPluginMeta('grafana-incident-app', 'Grafana Incident'), ...getMockPluginMeta('grafana-incident-app', 'Grafana Incident'),
}); });
}); });
describe('Grafana rules action buttons', () => { describe('Grafana rules action buttons', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' }); const grafanaRule = getGrafanaRule({ name: 'Grafana' });
@ -64,7 +48,8 @@ describe('RulesTable RBAC', () => {
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true]; return action === AlertRuleAction.Update ? [true, false] : [true, true];
}); });
renderRulesTable(grafanaRule);
render(<RulesTable rules={[grafanaRule]} />);
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument(); expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
}); });
@ -74,7 +59,8 @@ describe('RulesTable RBAC', () => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true]; return action === AlertRuleAction.Delete ? [true, false] : [true, true];
}); });
renderRulesTable(grafanaRule); render(<RulesTable rules={[grafanaRule]} />);
await user.click(ui.actionButtons.more.get()); await user.click(ui.actionButtons.more.get());
expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument(); expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument();
@ -84,7 +70,8 @@ describe('RulesTable RBAC', () => {
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => { mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false]; return action === AlertRuleAction.Update ? [true, true] : [false, false];
}); });
renderRulesTable(grafanaRule); render(<RulesTable rules={[grafanaRule]} />);
expect(ui.actionButtons.edit.get()).toBeInTheDocument(); expect(ui.actionButtons.edit.get()).toBeInTheDocument();
}); });
@ -93,12 +80,56 @@ describe('RulesTable RBAC', () => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false]; return action === AlertRuleAction.Delete ? [true, true] : [false, false];
}); });
renderRulesTable(grafanaRule); render(<RulesTable rules={[grafanaRule]} />);
expect(ui.actionButtons.more.get()).toBeInTheDocument(); expect(ui.actionButtons.more.get()).toBeInTheDocument();
await user.click(ui.actionButtons.more.get()); await user.click(ui.actionButtons.more.get());
expect(ui.moreActionItems.delete.get()).toBeInTheDocument(); expect(ui.moreActionItems.delete.get()).toBeInTheDocument();
}); });
describe('rules in creating/deleting states', () => {
const { promRule, ...creatingRule } = grafanaRule;
const { rulerRule, ...deletingRule } = grafanaRule;
const rulesSource = 'grafana';
/**
* Preloaded state that implies the rulerRules have finished loading
*
* @todo Remove this state and test at a higher level to avoid mocking the store.
* We need to manually populate this, as the component hierarchy expects that we will
* have already called the necessary APIs to get the rulerRules data
*/
const preloadedState = {
unifiedAlerting: { rulerRules: { [rulesSource]: { result: {}, loading: false, dispatched: true } } },
};
beforeEach(() => {
mocks.useAlertRuleAbility.mockImplementation(() => {
return [true, true];
});
});
it('does not render View button when rule is creating', async () => {
render(<RulesTable rules={[creatingRule]} />, {
// @ts-ignore
preloadedState,
});
expect(await screen.findByText('Creating')).toBeInTheDocument();
expect(ui.actionButtons.view.query()).not.toBeInTheDocument();
});
it('does not render View or Edit button when rule is deleting', async () => {
render(<RulesTable rules={[deletingRule]} />, {
// @ts-ignore
preloadedState,
});
expect(await screen.findByText('Deleting')).toBeInTheDocument();
expect(ui.actionButtons.view.query()).not.toBeInTheDocument();
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
});
}); });
describe('Cloud rules action buttons', () => { describe('Cloud rules action buttons', () => {
@ -109,7 +140,8 @@ describe('RulesTable RBAC', () => {
return action === AlertRuleAction.Update ? [true, false] : [true, true]; return action === AlertRuleAction.Update ? [true, false] : [true, true];
}); });
renderRulesTable(cloudRule); render(<RulesTable rules={[cloudRule]} />);
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument(); expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
}); });
@ -118,7 +150,8 @@ describe('RulesTable RBAC', () => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true]; return action === AlertRuleAction.Delete ? [true, false] : [true, true];
}); });
renderRulesTable(cloudRule); render(<RulesTable rules={[cloudRule]} />);
await user.click(ui.actionButtons.more.get()); await user.click(ui.actionButtons.more.get());
expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument(); expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument();
}); });
@ -128,7 +161,8 @@ describe('RulesTable RBAC', () => {
return action === AlertRuleAction.Update ? [true, true] : [false, false]; return action === AlertRuleAction.Update ? [true, true] : [false, false];
}); });
renderRulesTable(cloudRule); render(<RulesTable rules={[cloudRule]} />);
expect(ui.actionButtons.edit.get()).toBeInTheDocument(); expect(ui.actionButtons.edit.get()).toBeInTheDocument();
}); });
@ -137,7 +171,8 @@ describe('RulesTable RBAC', () => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false]; return action === AlertRuleAction.Delete ? [true, true] : [false, false];
}); });
renderRulesTable(cloudRule); render(<RulesTable rules={[cloudRule]} />);
await user.click(ui.actionButtons.more.get()); await user.click(ui.actionButtons.more.get());
expect(ui.moreActionItems.delete.get()).toBeInTheDocument(); expect(ui.moreActionItems.delete.get()).toBeInTheDocument();
}); });

View File

@ -109,18 +109,25 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNe
const { hasRuler, rulerRulesLoaded } = useHasRuler(); const { hasRuler, rulerRulesLoaded } = useHasRuler();
return useMemo((): RuleTableColumnProps[] => { return useMemo((): RuleTableColumnProps[] => {
const ruleIsDeleting = (rule: CombinedRule) => {
const { namespace, promRule, rulerRule } = rule;
const { rulesSource } = namespace;
return Boolean(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && promRule && !rulerRule);
};
const ruleIsCreating = (rule: CombinedRule) => {
const { namespace, promRule, rulerRule } = rule;
const { rulesSource } = namespace;
return Boolean(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && rulerRule && !promRule);
};
const columns: RuleTableColumnProps[] = [ const columns: RuleTableColumnProps[] = [
{ {
id: 'state', id: 'state',
label: 'State', label: 'State',
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => { renderCell: ({ data: rule }) => {
const { namespace } = rule; const isDeleting = ruleIsDeleting(rule);
const { rulesSource } = namespace; const isCreating = ruleIsCreating(rule);
const { promRule, rulerRule } = rule;
const isDeleting = !!(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && promRule && !rulerRule);
const isCreating = !!(hasRuler(rulesSource) && rulerRulesLoaded(rulesSource) && rulerRule && !promRule);
const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule); const isPaused = isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulerRulePaused(rule.rulerRule);
return <RuleState rule={rule} isDeleting={isDeleting} isCreating={isCreating} isPaused={isPaused} />; return <RuleState rule={rule} isDeleting={isDeleting} isCreating={isCreating} isPaused={isPaused} />;
@ -226,7 +233,16 @@ function useColumns(showSummaryColumn: boolean, showGroupColumn: boolean, showNe
label: 'Actions', label: 'Actions',
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => { renderCell: ({ data: rule }) => {
return <RuleActionsButtons compact showViewButton rule={rule} rulesSource={rule.namespace.rulesSource} />; const isDeleting = ruleIsDeleting(rule);
const isCreating = ruleIsCreating(rule);
return (
<RuleActionsButtons
compact
showViewButton={!isDeleting && !isCreating}
rule={rule}
rulesSource={rule.namespace.rulesSource}
/>
);
}, },
size: '200px', size: '200px',
}); });