Alerting: Move action buttons in the alert list view (#81341)

Move action buttons in the alert list view
This commit is contained in:
Sonia Aguilar 2024-01-26 14:56:00 +01:00 committed by GitHub
parent 5c9fe6ac93
commit f042ca5b12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 104 deletions

View File

@ -1,78 +0,0 @@
import { isEmpty } from 'lodash';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { useToggle } from 'react-use';
import { urlUtil } from '@grafana/data';
import { Button, Dropdown, Icon, LinkButton, Menu, MenuItem } from '@grafana/ui';
import { logInfo, LogMessages } from './Analytics';
import { GrafanaRulesExporter } from './components/export/GrafanaRulesExporter';
import { AlertingAction, useAlertingAbility } from './hooks/useAbilities';
interface Props {
enableExport?: boolean;
}
export function MoreActionsRuleButtons({ enableExport }: Props) {
const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule);
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
const [exportRulesSupported, exportRulesAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
const location = useLocation();
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
const canCreateGrafanaRules = createRuleSupported && createRuleAllowed;
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
const canExportRules = exportRulesSupported && exportRulesAllowed;
const menuItems: JSX.Element[] = [];
if (canCreateGrafanaRules || canCreateCloudRules) {
menuItems.push(
<MenuItem
label="New recording rule"
key="new-recording-rule"
url={urlUtil.renderUrl(`alerting/new/recording`, {
returnTo: location.pathname + location.search,
})}
/>
);
}
if (canExportRules) {
menuItems.push(
<MenuItem
label="Export all Grafana-managed rules"
key="export-all-rules"
onClick={toggleShowExportDrawer}
disabled={!enableExport}
description={enableExport ? '' : 'No Grafana-managed rules found'}
/>
);
}
return (
<>
{(canCreateGrafanaRules || canCreateCloudRules) && (
<LinkButton
href={urlUtil.renderUrl('alerting/new/alerting', { returnTo: location.pathname + location.search })}
icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
>
New alert rule
</LinkButton>
)}
{!isEmpty(menuItems) && (
<Dropdown overlay={<Menu>{menuItems}</Menu>}>
<Button variant="secondary">
More
<Icon name="angle-down" />
</Button>
</Dropdown>
)}
{canExportRules && showExportDrawer && <GrafanaRulesExporter onClose={toggleShowExportDrawer} />}
</>
);
}

View File

@ -25,9 +25,9 @@ import { discoverFeatures } from './api/buildInfo';
import { fetchRules } from './api/prometheus';
import { deleteNamespace, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from './api/ruler';
import {
MockDataSourceSrv,
grantUserPermissions,
mockDataSource,
MockDataSourceSrv,
mockPromAlert,
mockPromAlertingRule,
mockPromRecordingRule,
@ -120,11 +120,8 @@ const ui = {
rulesFilterInput: byTestId('search-query-input'),
moreErrorsButton: byRole('button', { name: /more errors/ }),
editCloudGroupIcon: byTestId('edit-group'),
newRuleButton: byRole('link', { name: 'New alert rule' }),
moreButton: byRole('button', { name: 'More' }),
exportButton: byRole('menuitem', {
name: /export all grafana\-managed rules/i,
}),
newRuleButton: byText(/new alert rule/i),
exportButton: byText(/export rules/i),
editGroupModal: {
dialog: byRole('dialog'),
namespaceInput: byRole('textbox', { name: /^Namespace/ }),
@ -728,7 +725,8 @@ describe('RuleList', () => {
renderRuleList();
await userEvent.click(ui.moreButton.get());
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
expect(ui.exportButton.get()).toBeInTheDocument();
});
});

View File

@ -1,16 +1,16 @@
import { css } from '@emotion/css';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useAsyncFn, useInterval } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2, withErrorBoundary, Stack } from '@grafana/ui';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useDispatch } from 'app/types';
import { CombinedRuleNamespace } from '../../../types/unified-alerting';
import { trackRuleListNavigation } from './Analytics';
import { MoreActionsRuleButtons } from './MoreActionsRuleButtons';
import { LogMessages, logInfo, trackRuleListNavigation } from './Analytics';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoRulesSplash } from './components/rules/NoRulesCTA';
import { INSTANCES_DISPLAY_LIMIT } from './components/rules/RuleDetails';
@ -19,12 +19,13 @@ import { RuleListGroupView } from './components/rules/RuleListGroupView';
import { RuleListStateView } from './components/rules/RuleListStateView';
import { RuleStats } from './components/rules/RuleStats';
import RulesFilter from './components/rules/RulesFilter';
import { AlertingAction, useAlertingAbility } from './hooks/useAbilities';
import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
import { useFilteredRules, useRulesFilter } from './hooks/useFilteredRules';
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
import { fetchAllPromAndRulerRulesAction } from './state/actions';
import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants';
import { getAllRulesSourceNames, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { getAllRulesSourceNames } from './utils/datasource';
const VIEWS = {
groups: RuleListGroupView,
@ -77,9 +78,6 @@ const RuleList = withErrorBoundary(
return noRules && state.dispatched;
});
const grafanaRules = useUnifiedAlertingSelector((state) => state.rulerRules[GRAFANA_RULES_SOURCE_NAME]?.result);
const hasGrafanaRules = Object.keys(grafanaRules ?? {}).length > 0;
const limitAlerts = hasActiveFilters ? undefined : LIMIT_ALERTS;
// Trigger data refresh only when the RULE_LIST_POLL_INTERVAL_MS elapsed since the previous load FINISHED
const [_, fetchRules] = useAsyncFn(async () => {
@ -101,6 +99,7 @@ const RuleList = withErrorBoundary(
// Show splash only when we loaded all of the data sources and none of them has alerts
const hasNoAlertRulesCreatedYet =
allPromLoaded && allPromEmpty && promRequests.length > 0 && allRulerEmpty && allRulerLoaded;
const hasAlertRulesCreated = !hasNoAlertRulesCreatedYet;
const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces();
const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState);
@ -108,10 +107,10 @@ const RuleList = withErrorBoundary(
return (
// We don't want to show the Loading... indicator for the whole page.
// We show separate indicators for Grafana-managed and Cloud rules
<AlertingPageWrapper navId="alert-list" isLoading={false}>
<AlertingPageWrapper navId="alert-list" isLoading={false} actions={hasAlertRulesCreated && <CreateAlertButton />}>
<RuleListErrors />
<RulesFilter onFilterCleared={onFilterCleared} />
{!hasNoAlertRulesCreatedYet && (
{hasAlertRulesCreated && (
<>
<div className={styles.break} />
<div className={styles.buttonsContainer}>
@ -128,14 +127,11 @@ const RuleList = withErrorBoundary(
)}
<RuleStats namespaces={filteredNamespaces} />
</div>
<Stack direction="row" gap={0.5}>
<MoreActionsRuleButtons enableExport={hasGrafanaRules} />
</Stack>
</div>
</>
)}
{hasNoAlertRulesCreatedYet && <NoRulesSplash />}
{!hasNoAlertRulesCreatedYet && <ViewComponent expandAll={expandAll} namespaces={filteredNamespaces} />}
{hasAlertRulesCreated && <ViewComponent expandAll={expandAll} namespaces={filteredNamespaces} />}
</AlertingPageWrapper>
);
},
@ -165,3 +161,27 @@ const getStyles = (theme: GrafanaTheme2) => ({
});
export default RuleList;
export function CreateAlertButton() {
const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule);
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
const location = useLocation();
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
const canCreateGrafanaRules = createRuleSupported && createRuleAllowed;
if (canCreateGrafanaRules || canCreateCloudRules) {
return (
<LinkButton
href={urlUtil.renderUrl('alerting/new/alerting', { returnTo: location.pathname + location.search })}
icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
>
New alert rule
</LinkButton>
);
}
return null;
}

View File

@ -1,12 +1,14 @@
import { css } from '@emotion/css';
import pluralize from 'pluralize';
import React, { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { LoadingPlaceholder, Pagination, Spinner, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { LinkButton, LoadingPlaceholder, Pagination, Spinner, useStyles2 } from '@grafana/ui';
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
import { AlertingAction, useAlertingAbility } from '../../hooks/useAbilities';
import { usePagination } from '../../hooks/usePagination';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { getPaginationStyles } from '../../styles/pagination';
@ -52,7 +54,10 @@ export const CloudRules = ({ namespaces, expandAll }: Props) => {
return (
<section className={styles.wrapper}>
<div className={styles.sectionHeader}>
<h5>Mimir / Cortex / Loki</h5>
<div className={styles.headerRow}>
<h5>Mimir / Cortex / Loki</h5>
<CreateRecordingRuleButton />
</div>
{dataSourcesLoading.length ? (
<LoadingPlaceholder
className={styles.loader}
@ -106,4 +111,35 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: ${theme.spacing(2)};
`,
pagination: getPaginationStyles(theme),
headerRow: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
marginBottom: theme.spacing(1),
}),
});
export function CreateRecordingRuleButton() {
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
const location = useLocation();
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
if (canCreateCloudRules) {
return (
<LinkButton
key="new-recording-rule"
href={urlUtil.renderUrl(`alerting/new/recording`, {
returnTo: location.pathname + location.search,
})}
icon="plus"
variant="secondary"
>
New recording rule
</LinkButton>
);
}
return null;
}

View File

@ -1,18 +1,21 @@
import { css } from '@emotion/css';
import React from 'react';
import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { LoadingPlaceholder, Pagination, Spinner, useStyles2 } from '@grafana/ui';
import { Button, LoadingPlaceholder, Pagination, Spinner, useStyles2 } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
import { AlertingAction, useAlertingAbility } from '../../hooks/useAbilities';
import { flattenGrafanaManagedRules } from '../../hooks/useCombinedRuleNamespaces';
import { usePagination } from '../../hooks/usePagination';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { getPaginationStyles } from '../../styles/pagination';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { initialAsyncRequestState } from '../../utils/redux';
import { GrafanaRulesExporter } from '../export/GrafanaRulesExporter';
import { RulesGroup } from './RulesGroup';
import { useCombinedGroupNamespace } from './useCombinedGroupNamespace';
@ -45,10 +48,30 @@ export const GrafanaRules = ({ namespaces, expandAll }: Props) => {
DEFAULT_PER_PAGE_PAGINATION
);
const [exportRulesSupported, exportRulesAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
const canExportRules = exportRulesSupported && exportRulesAllowed;
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
const hasGrafanaAlerts = namespaces.length > 0;
return (
<section className={styles.wrapper}>
<div className={styles.sectionHeader}>
<h5>Grafana</h5>
<div className={styles.headerRow}>
<h5>Grafana</h5>
{hasGrafanaAlerts && canExportRules && (
<Button
aria-label="export all grafana rules"
data-testid="export-all-grafana-rules"
icon="download-alt"
tooltip="Export all Grafana-managed rules"
onClick={toggleShowExportDrawer}
variant="secondary"
>
Export rules
</Button>
)}
</div>
{loading ? <LoadingPlaceholder className={styles.loader} text="Loading..." /> : <div />}
</div>
@ -70,6 +93,7 @@ export const GrafanaRules = ({ namespaces, expandAll }: Props) => {
onNavigate={onPageChange}
hideWhenSinglePage
/>
{canExportRules && showExportDrawer && <GrafanaRulesExporter onClose={toggleShowExportDrawer} />}
</section>
);
};
@ -91,4 +115,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: ${theme.spacing(2)};
`,
pagination: getPaginationStyles(theme),
headerRow: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
flexDirection: 'row',
}),
});