mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Move action buttons in the alert list view (#81341)
Move action buttons in the alert list view
This commit is contained in:
parent
5c9fe6ac93
commit
f042ca5b12
@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
}),
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user