mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Promote new alerting detail view (#84277)
This commit is contained in:
parent
8690a42e33
commit
336acaf0bf
@ -2115,21 +2115,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "5"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "6"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "7"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "8"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "9"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "10"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "11"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "12"]
|
||||
],
|
||||
"public/app/features/alerting/unified/components/rule-viewer/RuleViewerLayout.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
|
@ -152,7 +152,6 @@ export interface FeatureToggles {
|
||||
canvasPanelPanZoom?: boolean;
|
||||
logsInfiniteScrolling?: boolean;
|
||||
flameGraphItemCollapsing?: boolean;
|
||||
alertingDetailsViewV2?: boolean;
|
||||
datatrails?: boolean;
|
||||
alertingSimplifiedRouting?: boolean;
|
||||
logRowsPopoverMenu?: boolean;
|
||||
|
@ -991,14 +991,6 @@ var (
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||
},
|
||||
{
|
||||
Name: "alertingDetailsViewV2",
|
||||
Description: "Enables the preview of the new alert details view",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaAlertingSquad,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
{
|
||||
Name: "datatrails",
|
||||
Description: "Enables the new core app datatrails",
|
||||
|
@ -133,7 +133,6 @@ ssoSettingsApi,preview,@grafana/identity-access-team,false,false,false
|
||||
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
|
||||
logsInfiniteScrolling,experimental,@grafana/observability-logs,false,false,true
|
||||
flameGraphItemCollapsing,experimental,@grafana/observability-traces-and-profiling,false,false,true
|
||||
alertingDetailsViewV2,experimental,@grafana/alerting-squad,false,false,true
|
||||
datatrails,experimental,@grafana/dashboards-squad,false,false,true
|
||||
alertingSimplifiedRouting,preview,@grafana/alerting-squad,false,false,false
|
||||
logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true
|
||||
|
|
@ -543,10 +543,6 @@ const (
|
||||
// Allow collapsing of flame graph items
|
||||
FlagFlameGraphItemCollapsing = "flameGraphItemCollapsing"
|
||||
|
||||
// FlagAlertingDetailsViewV2
|
||||
// Enables the preview of the new alert details view
|
||||
FlagAlertingDetailsViewV2 = "alertingDetailsViewV2"
|
||||
|
||||
// FlagDatatrails
|
||||
// Enables the new core app datatrails
|
||||
FlagDatatrails = "datatrails"
|
||||
|
@ -999,7 +999,8 @@
|
||||
"metadata": {
|
||||
"name": "alertingDetailsViewV2",
|
||||
"resourceVersion": "1709648236447",
|
||||
"creationTimestamp": "2024-03-05T14:17:16Z"
|
||||
"creationTimestamp": "2024-03-05T14:17:16Z",
|
||||
"deletionTimestamp": "2024-03-12T12:33:03Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the preview of the new alert details view",
|
||||
|
@ -1,40 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { config, isFetchError } from '@grafana/runtime';
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
import { Alert, withErrorBoundary } from '@grafana/ui';
|
||||
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { AlertRuleProvider } from './components/rule-viewer/v2/RuleContext';
|
||||
import { AlertRuleProvider } from './components/rule-viewer/RuleContext';
|
||||
import DetailView from './components/rule-viewer/RuleViewer';
|
||||
import { useCombinedRule } from './hooks/useCombinedRule';
|
||||
import { stringifyErrorLike } from './utils/misc';
|
||||
import { getRuleIdFromPathname, parse as parseRuleId } from './utils/rule-id';
|
||||
|
||||
const DetailViewV1 = SafeDynamicImport(() => import('./components/rule-viewer/RuleViewer.v1'));
|
||||
const DetailViewV2 = React.lazy(() => import('./components/rule-viewer/v2/RuleViewer.v2'));
|
||||
|
||||
type RuleViewerProps = GrafanaRouteComponentProps<{
|
||||
id: string;
|
||||
sourceName: string;
|
||||
}>;
|
||||
|
||||
const newAlertDetailView = Boolean(config.featureToggles?.alertingDetailsViewV2) === true;
|
||||
|
||||
const RuleViewer = (props: RuleViewerProps): JSX.Element => {
|
||||
return newAlertDetailView ? <RuleViewerV2Wrapper {...props} /> : <RuleViewerV1Wrapper {...props} />;
|
||||
};
|
||||
|
||||
export const defaultPageNav: NavModelItem = {
|
||||
id: 'alert-rule-view',
|
||||
text: '',
|
||||
};
|
||||
|
||||
const RuleViewerV1Wrapper = (props: RuleViewerProps) => <DetailViewV1 {...props} />;
|
||||
|
||||
const RuleViewerV2Wrapper = (props: RuleViewerProps) => {
|
||||
const id = getRuleIdFromPathname(props.match.params);
|
||||
|
||||
// we convert the stringified ID to a rule identifier object which contains additional
|
||||
@ -69,7 +53,7 @@ const RuleViewerV2Wrapper = (props: RuleViewerProps) => {
|
||||
if (rule) {
|
||||
return (
|
||||
<AlertRuleProvider identifier={identifier} rule={rule}>
|
||||
<DetailViewV2 />
|
||||
<DetailView />
|
||||
</AlertRuleProvider>
|
||||
);
|
||||
}
|
||||
@ -82,6 +66,11 @@ const RuleViewerV2Wrapper = (props: RuleViewerProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const defaultPageNav: NavModelItem = {
|
||||
id: 'alert-rule-view',
|
||||
text: '',
|
||||
};
|
||||
|
||||
interface ErrorMessageProps {
|
||||
error: unknown;
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ import { Dropdown, LinkButton, Menu } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { AlertRuleAction, useAlertRuleAbility } from '../../../hooks/useAbilities';
|
||||
import { createShareLink, isLocalDevEnv, isOpenSourceEdition, makeRuleBasedSilenceLink } from '../../../utils/misc';
|
||||
import * as ruleId from '../../../utils/rule-id';
|
||||
import { createUrl } from '../../../utils/url';
|
||||
import MoreButton from '../../MoreButton';
|
||||
import { DeclareIncidentMenuItem } from '../../bridges/DeclareIncidentButton';
|
||||
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
|
||||
import { createShareLink, isLocalDevEnv, isOpenSourceEdition, makeRuleBasedSilenceLink } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { createUrl } from '../../utils/url';
|
||||
import MoreButton from '../MoreButton';
|
||||
import { DeclareIncidentMenuItem } from '../bridges/DeclareIncidentButton';
|
||||
|
||||
import { useAlertRule } from './RuleContext';
|
||||
|
@ -4,9 +4,9 @@ import { ConfirmModal } from '@grafana/ui';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
|
||||
import { deleteRuleAction } from '../../../state/actions';
|
||||
import { getRulesSourceName } from '../../../utils/datasource';
|
||||
import { fromRulerRule } from '../../../utils/rule-id';
|
||||
import { deleteRuleAction } from '../../state/actions';
|
||||
import { getRulesSourceName } from '../../utils/datasource';
|
||||
import { fromRulerRule } from '../../utils/rule-id';
|
||||
|
||||
type DeleteModalHook = [JSX.Element, (rule: CombinedRule) => void, () => void];
|
||||
|
@ -9,12 +9,12 @@ import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
|
||||
import { getCloudRule, getGrafanaRule, grantUserPermissions } from '../../../mocks';
|
||||
import { Annotation } from '../../../utils/constants';
|
||||
import * as ruleId from '../../../utils/rule-id';
|
||||
import { getCloudRule, getGrafanaRule, grantUserPermissions } from '../../mocks';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
|
||||
import { AlertRuleProvider } from './RuleContext';
|
||||
import RuleViewer from './RuleViewer.v2';
|
||||
import RuleViewer from './RuleViewer';
|
||||
import { createMockGrafanaServer } from './__mocks__/server';
|
||||
|
||||
// metadata and interactive elements
|
@ -9,28 +9,28 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { CombinedRule, RuleHealth, RuleIdentifier } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { defaultPageNav } from '../../../RuleViewer';
|
||||
import { Annotation } from '../../../utils/constants';
|
||||
import { makeDashboardLink, makePanelLink } from '../../../utils/misc';
|
||||
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule, isRecordingRule } from '../../../utils/rules';
|
||||
import { createUrl } from '../../../utils/url';
|
||||
import { AlertLabels } from '../../AlertLabels';
|
||||
import { AlertingPageWrapper } from '../../AlertingPageWrapper';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
|
||||
import { WithReturnButton } from '../../WithReturnButton';
|
||||
import { decodeGrafanaNamespace } from '../../expressions/util';
|
||||
import { RedirectToCloneRule } from '../../rules/CloneRule';
|
||||
import { FederatedRuleWarning } from '../FederatedRuleWarning';
|
||||
import { Details } from '../tabs/Details';
|
||||
import { History } from '../tabs/History';
|
||||
import { InstancesList } from '../tabs/Instances';
|
||||
import { QueryResults } from '../tabs/Query';
|
||||
import { Routing } from '../tabs/Routing';
|
||||
import { defaultPageNav } from '../../RuleViewer';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { makeDashboardLink, makePanelLink } from '../../utils/misc';
|
||||
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule, isRecordingRule } from '../../utils/rules';
|
||||
import { createUrl } from '../../utils/url';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { AlertingPageWrapper } from '../AlertingPageWrapper';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
import { WithReturnButton } from '../WithReturnButton';
|
||||
import { decodeGrafanaNamespace } from '../expressions/util';
|
||||
import { RedirectToCloneRule } from '../rules/CloneRule';
|
||||
|
||||
import { useAlertRulePageActions } from './Actions';
|
||||
import { useDeleteModal } from './DeleteModal';
|
||||
import { FederatedRuleWarning } from './FederatedRuleWarning';
|
||||
import { useAlertRule } from './RuleContext';
|
||||
import { RecordingBadge, StateBadge } from './StateBadges';
|
||||
import { Details } from './tabs/Details';
|
||||
import { History } from './tabs/History';
|
||||
import { InstancesList } from './tabs/Instances';
|
||||
import { QueryResults } from './tabs/Query';
|
||||
import { Routing } from './tabs/Routing';
|
||||
|
||||
enum ActiveTab {
|
||||
Query = 'query',
|
@ -1,378 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { byRole, byText } from 'testing-library-selector';
|
||||
|
||||
import { PluginExtensionTypes } from '@grafana/data';
|
||||
import { getPluginLinkExtensions, locationService, setBackendSrv } from '@grafana/runtime';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { discoverFeatures } from '../../api/buildInfo';
|
||||
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
|
||||
import { mockAlertRuleApi, setupMswServer } from '../../mockApi';
|
||||
import {
|
||||
getCloudRule,
|
||||
getGrafanaRule,
|
||||
grantUserPermissions,
|
||||
mockDataSource,
|
||||
mockPromAlertingRule,
|
||||
mockRulerAlertingRule,
|
||||
promRuleFromRulerRule,
|
||||
} from '../../mocks';
|
||||
import { mockAlertmanagerChoiceResponse } from '../../mocks/alertmanagerApi';
|
||||
import { mockPluginSettings } from '../../mocks/plugins';
|
||||
import { setupDataSources } from '../../testSetup/datasources';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
|
||||
import { RuleViewer } from './RuleViewer.v1';
|
||||
|
||||
const mockGrafanaRule = getGrafanaRule({ name: 'Test alert' }, { uid: 'test1', title: 'Test alert' });
|
||||
const mockCloudRule = getCloudRule({ name: 'cloud test alert' });
|
||||
|
||||
const mockRoute = (id?: string): GrafanaRouteComponentProps<{ id?: string; sourceName?: string }> => ({
|
||||
route: {
|
||||
path: '/',
|
||||
component: RuleViewer,
|
||||
},
|
||||
queryParams: { returnTo: '/alerting/list' },
|
||||
match: { params: { id: id ?? 'test1', sourceName: 'grafana' }, isExact: false, url: 'asdf', path: '' },
|
||||
history: locationService.getHistory(),
|
||||
location: { pathname: '', hash: '', search: '', state: '' },
|
||||
staticContext: {},
|
||||
});
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
useReturnToPrevious: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../hooks/useAbilities');
|
||||
jest.mock('../../api/buildInfo');
|
||||
|
||||
const mocks = {
|
||||
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
|
||||
useAlertRuleAbility: jest.mocked(useAlertRuleAbility),
|
||||
};
|
||||
|
||||
const ui = {
|
||||
actionButtons: {
|
||||
edit: byRole('link', { name: /edit/i }),
|
||||
silence: byRole('link', { name: 'Silence' }),
|
||||
},
|
||||
moreButton: byRole('button', { name: /More/i }),
|
||||
moreButtons: {
|
||||
duplicate: byRole('menuitem', { name: /^Duplicate$/i }),
|
||||
delete: byRole('menuitem', { name: /delete/i }),
|
||||
},
|
||||
loadingIndicator: byText(/Loading rule/i),
|
||||
};
|
||||
|
||||
const renderRuleViewer = async (ruleId: string) => {
|
||||
locationService.push(`/alerting/grafana/${ruleId}/view`);
|
||||
render(
|
||||
<TestProvider>
|
||||
<RuleViewer {...mockRoute(ruleId)} />
|
||||
</TestProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
||||
};
|
||||
|
||||
const server = setupMswServer();
|
||||
const user = userEvent.setup();
|
||||
|
||||
const dsName = 'prometheus';
|
||||
const rulerRule = mockRulerAlertingRule({ alert: 'cloud test alert' });
|
||||
const rulerRuleIdentifier = ruleId.fromRulerRule('prometheus', 'ns-default', 'group-default', rulerRule);
|
||||
|
||||
beforeAll(() => {
|
||||
setBackendSrv(backendSrv);
|
||||
const promDsSettings = mockDataSource({
|
||||
name: dsName,
|
||||
uid: dsName,
|
||||
});
|
||||
|
||||
setupDataSources(promDsSettings);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// some action buttons need to check what Alertmanager setup we have for Grafana managed rules
|
||||
mockAlertmanagerChoiceResponse(server, {
|
||||
alertmanagersChoice: AlertmanagerChoice.Internal,
|
||||
numExternalAlertmanagers: 1,
|
||||
});
|
||||
// we need to mock this one for the "declare incident" button
|
||||
mockPluginSettings(server, SupportedPlugin.Incident);
|
||||
|
||||
mockAlertRuleApi(server).rulerRules('grafana', {
|
||||
[mockGrafanaRule.namespace.name]: [
|
||||
{ name: mockGrafanaRule.group.name, interval: '1m', rules: [mockGrafanaRule.rulerRule!] },
|
||||
],
|
||||
});
|
||||
|
||||
const { name, query, labels, annotations } = mockGrafanaRule;
|
||||
mockAlertRuleApi(server).prometheusRuleNamespaces('grafana', {
|
||||
data: {
|
||||
groups: [
|
||||
{
|
||||
file: mockGrafanaRule.namespace.name,
|
||||
interval: 60,
|
||||
name: mockGrafanaRule.group.name,
|
||||
rules: [mockPromAlertingRule({ name, query, labels, annotations })],
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
mockAlertRuleApi(server).rulerRuleGroup(dsName, 'ns-default', 'group-default', {
|
||||
name: 'group-default',
|
||||
interval: '1m',
|
||||
rules: [rulerRule],
|
||||
});
|
||||
|
||||
mockAlertRuleApi(server).prometheusRuleNamespaces(dsName, {
|
||||
data: {
|
||||
groups: [
|
||||
{
|
||||
file: 'ns-default',
|
||||
interval: 60,
|
||||
name: 'group-default',
|
||||
rules: [promRuleFromRulerRule(rulerRule, { state: PromAlertingRuleState.Inactive })],
|
||||
},
|
||||
],
|
||||
},
|
||||
status: 'success',
|
||||
});
|
||||
mocks.getPluginLinkExtensionsMock.mockReturnValue({
|
||||
extensions: [
|
||||
{
|
||||
pluginId: 'grafana-ml-app',
|
||||
id: '1',
|
||||
type: PluginExtensionTypes.link,
|
||||
title: 'Run investigation',
|
||||
category: 'Sift',
|
||||
description: 'Run a Sift investigation for this alert',
|
||||
onClick: jest.fn(),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('RuleViewer', () => {
|
||||
let mockCombinedRule = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
mockCombinedRule.mockReset();
|
||||
});
|
||||
|
||||
it('should render page with grafana alert', async () => {
|
||||
mocks.useAlertRuleAbility.mockReturnValue([true, true]);
|
||||
await renderRuleViewer('test1');
|
||||
|
||||
expect(screen.getByText(/test alert/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render page with cloud alert', async () => {
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
|
||||
jest
|
||||
.mocked(discoverFeatures)
|
||||
.mockResolvedValue({ application: PromApplication.Mimir, features: { rulerApiEnabled: true } });
|
||||
|
||||
mocks.useAlertRuleAbility.mockReturnValue([true, true]);
|
||||
await renderRuleViewer(ruleId.stringifyIdentifier(rulerRuleIdentifier));
|
||||
|
||||
expect(screen.getByText(/cloud test alert/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RuleDetails RBAC', () => {
|
||||
describe('Grafana rules action buttons in details', () => {
|
||||
let mockCombinedRule = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
// mockCombinedRule = jest.mocked(useCombinedRule);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockCombinedRule.mockReset();
|
||||
});
|
||||
it('Should render Edit button for users with the update permission', async () => {
|
||||
// Arrange
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
// Act
|
||||
await renderRuleViewer('test1');
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', async () => {
|
||||
// Arrange
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
|
||||
// Act
|
||||
await renderRuleViewer('test1');
|
||||
await user.click(ui.moreButton.get());
|
||||
|
||||
// Assert
|
||||
expect(ui.moreButtons.delete.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not render Silence button for users wihout the instance create permission', async () => {
|
||||
// Arrange
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
await renderRuleViewer('test1');
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render Silence button for users with the instance create permissions', async () => {
|
||||
// Arrange
|
||||
mocks.useAlertRuleAbility.mockReturnValue([true, true]);
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockGrafanaRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
jest
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
|
||||
|
||||
// Act
|
||||
await renderRuleViewer('test1');
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(ui.actionButtons.silence.get()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render clone button for users having create rule permission', async () => {
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Duplicate ? [true, true] : [false, false];
|
||||
});
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: getGrafanaRule({ name: 'Grafana rule' }),
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
});
|
||||
grantUserPermissions([AccessControlAction.AlertingRuleCreate]);
|
||||
|
||||
await renderRuleViewer('test1');
|
||||
await user.click(ui.moreButton.get());
|
||||
|
||||
expect(ui.moreButtons.duplicate.get()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should NOT render clone button for users without create rule permission', async () => {
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Duplicate ? [true, false] : [true, true];
|
||||
});
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: getGrafanaRule({ name: 'Grafana rule' }),
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
});
|
||||
|
||||
const { AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete } = AccessControlAction;
|
||||
grantUserPermissions([AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete]);
|
||||
|
||||
await renderRuleViewer('test1');
|
||||
await user.click(ui.moreButton.get());
|
||||
|
||||
expect(ui.moreButtons.duplicate.query()).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cloud rules action buttons', () => {
|
||||
const mockCombinedRule = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
mockCombinedRule.mockReset();
|
||||
});
|
||||
|
||||
it('Should render edit button for users with the update permission', async () => {
|
||||
// Arrange
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Update ? [true, true] : [false, false];
|
||||
});
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockCloudRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
// Act
|
||||
await renderRuleViewer('test1');
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', async () => {
|
||||
// Arrange
|
||||
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
|
||||
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
|
||||
});
|
||||
mockCombinedRule.mockReturnValue({
|
||||
result: mockCloudRule as CombinedRule,
|
||||
loading: false,
|
||||
dispatched: true,
|
||||
requestId: 'A',
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
// Act
|
||||
await renderRuleViewer('test1');
|
||||
await user.click(ui.moreButton.get());
|
||||
|
||||
// Assert
|
||||
expect(ui.moreButtons.delete.query()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,262 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Collapse,
|
||||
Icon,
|
||||
IconButton,
|
||||
LoadingPlaceholder,
|
||||
useStyles2,
|
||||
VerticalGroup,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@grafana/ui';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||
import { GrafanaRuleDefinition } from '../../../../../types/unified-alerting-dto';
|
||||
import { useCombinedRule } from '../../hooks/useCombinedRule';
|
||||
import { useCleanAnnotations } from '../../utils/annotations';
|
||||
import { getRulesSourceByName } from '../../utils/datasource';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||
import { decodeGrafanaNamespace } from '../expressions/util';
|
||||
import { RuleViewerLayout } from '../rule-viewer/RuleViewerLayout';
|
||||
import { RuleDetailsActionButtons } from '../rules/RuleDetailsActionButtons';
|
||||
import { RuleDetailsAnnotations } from '../rules/RuleDetailsAnnotations';
|
||||
import { RuleDetailsDataSources } from '../rules/RuleDetailsDataSources';
|
||||
import { RuleDetailsExpression } from '../rules/RuleDetailsExpression';
|
||||
import { RuleDetailsFederatedSources } from '../rules/RuleDetailsFederatedSources';
|
||||
import { RuleDetailsMatchingInstances } from '../rules/RuleDetailsMatchingInstances';
|
||||
import { RuleHealth } from '../rules/RuleHealth';
|
||||
import { RuleState } from '../rules/RuleState';
|
||||
|
||||
import { QueryResults } from './tabs/Query';
|
||||
|
||||
type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>;
|
||||
|
||||
const errorMessage = 'Could not find data source for rule';
|
||||
const errorTitle = 'Could not view rule';
|
||||
const pageTitle = 'View rule';
|
||||
|
||||
export function RuleViewer({ match }: RuleViewerProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [expandQuery, setExpandQuery] = useToggle(false);
|
||||
|
||||
const identifier = useMemo(() => {
|
||||
const id = ruleId.getRuleIdFromPathname(match.params);
|
||||
if (!id) {
|
||||
throw new Error('Rule ID is required');
|
||||
}
|
||||
|
||||
return ruleId.parse(id, true);
|
||||
}, [match.params]);
|
||||
|
||||
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
|
||||
|
||||
const annotations = useCleanAnnotations(rule?.annotations || {});
|
||||
|
||||
if (!identifier?.ruleSourceName) {
|
||||
return (
|
||||
<RuleViewerLayout title={pageTitle}>
|
||||
<Alert title={errorTitle}>
|
||||
<details className={styles.errorMessage}>{errorMessage}</details>
|
||||
</Alert>
|
||||
</RuleViewerLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const rulesSource = getRulesSourceByName(identifier.ruleSourceName);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<RuleViewerLayout title={pageTitle}>
|
||||
<LoadingPlaceholder text="Loading rule..." />
|
||||
</RuleViewerLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !rulesSource) {
|
||||
return (
|
||||
<Alert title={errorTitle}>
|
||||
<details className={styles.errorMessage}>
|
||||
{isFetchError(error) ? error.message : errorMessage}
|
||||
<br />
|
||||
{/* TODO Fix typescript */}
|
||||
{/* {error && error?.stack} */}
|
||||
</details>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!rule) {
|
||||
return (
|
||||
<RuleViewerLayout title={pageTitle}>
|
||||
<span>Rule could not be found.</span>
|
||||
</RuleViewerLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const isFederatedRule = isFederatedRuleGroup(rule.group);
|
||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||
|
||||
return (
|
||||
<RuleViewerLayout
|
||||
wrapInContent={false}
|
||||
title={pageTitle}
|
||||
renderTitle={() => (
|
||||
<Stack direction="row" alignItems="flex-start" gap={1}>
|
||||
<Icon name="bell" size="xl" />
|
||||
<Text variant="h3">{rule.name}</Text>
|
||||
<RuleState rule={rule} isCreating={false} isDeleting={false} />
|
||||
</Stack>
|
||||
)}
|
||||
>
|
||||
{isFederatedRule && (
|
||||
<Alert severity="info" title="This rule is part of a federated rule group.">
|
||||
<VerticalGroup>
|
||||
Federated rule groups are currently an experimental feature.
|
||||
<Button fill="text" icon="book">
|
||||
<a href="https://grafana.com/docs/metrics-enterprise/latest/tenant-management/tenant-federation/#cross-tenant-alerting-and-recording-rule-federation">
|
||||
Read documentation
|
||||
</a>
|
||||
</Button>
|
||||
</VerticalGroup>
|
||||
</Alert>
|
||||
)}
|
||||
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.AlertRule} />}
|
||||
<>
|
||||
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} isViewMode={true} />
|
||||
<div className={styles.details}>
|
||||
<div className={styles.leftSide}>
|
||||
{rule.promRule && (
|
||||
<DetailsField label="Health" horizontal={true}>
|
||||
<RuleHealth rule={rule.promRule} />
|
||||
</DetailsField>
|
||||
)}
|
||||
{!!rule.labels && !!Object.keys(rule.labels).length && (
|
||||
<DetailsField label="Labels" horizontal={true}>
|
||||
<AlertLabels labels={rule.labels} />
|
||||
</DetailsField>
|
||||
)}
|
||||
<RuleDetailsExpression rulesSource={rulesSource} rule={rule} annotations={annotations} />
|
||||
<RuleDetailsAnnotations annotations={annotations} />
|
||||
</div>
|
||||
<div className={styles.rightSide}>
|
||||
<RuleDetailsDataSources rule={rule} rulesSource={rulesSource} />
|
||||
{isFederatedRule && <RuleDetailsFederatedSources group={rule.group} />}
|
||||
<DetailsField label="Namespace / Group" className={styles.rightSideDetails}>
|
||||
{decodeGrafanaNamespace(rule.namespace).name} / {rule.group.name}
|
||||
</DetailsField>
|
||||
{isGrafanaRulerRule(rule.rulerRule) && <GrafanaRuleUID rule={rule.rulerRule.grafana_alert} />}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DetailsField label="Matching instances" horizontal={true}>
|
||||
<RuleDetailsMatchingInstances
|
||||
rule={rule}
|
||||
pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }}
|
||||
enableFiltering
|
||||
/>
|
||||
</DetailsField>
|
||||
</div>
|
||||
</>
|
||||
<Collapse
|
||||
label="Query & Results"
|
||||
isOpen={expandQuery}
|
||||
onToggle={setExpandQuery}
|
||||
collapsible={true}
|
||||
className={styles.collapse}
|
||||
>
|
||||
{expandQuery && <QueryResults rule={rule} />}
|
||||
</Collapse>
|
||||
</RuleViewerLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function GrafanaRuleUID({ rule }: { rule: GrafanaRuleDefinition }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const copyUID = () => navigator.clipboard && navigator.clipboard.writeText(rule.uid);
|
||||
|
||||
return (
|
||||
<DetailsField label="Rule UID" childrenWrapperClassName={styles.ruleUid}>
|
||||
{rule.uid} <IconButton name="copy" onClick={copyUID} tooltip="Copy rule UID" />
|
||||
</DetailsField>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
errorMessage: css`
|
||||
white-space: pre-wrap;
|
||||
`,
|
||||
queries: css`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`,
|
||||
collapse: css`
|
||||
margin-top: ${theme.spacing(2)};
|
||||
border-color: ${theme.colors.border.weak};
|
||||
border-radius: ${theme.shape.radius.default};
|
||||
`,
|
||||
queriesTitle: css`
|
||||
padding: ${theme.spacing(2, 0.5)};
|
||||
font-size: ${theme.typography.h5.fontSize};
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
font-family: ${theme.typography.h5.fontFamily};
|
||||
`,
|
||||
query: css`
|
||||
border-bottom: 1px solid ${theme.colors.border.medium};
|
||||
padding: ${theme.spacing(2)};
|
||||
`,
|
||||
queryWarning: css`
|
||||
margin: ${theme.spacing(4, 0)};
|
||||
`,
|
||||
title: css`
|
||||
font-size: ${theme.typography.h4.fontSize};
|
||||
font-weight: ${theme.typography.fontWeightBold};
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
details: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${theme.spacing(4)};
|
||||
`,
|
||||
leftSide: css`
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`,
|
||||
rightSide: css`
|
||||
padding-right: ${theme.spacing(3)};
|
||||
|
||||
max-width: 360px;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
`,
|
||||
rightSideDetails: css`
|
||||
& > div:first-child {
|
||||
width: auto;
|
||||
}
|
||||
`,
|
||||
labels: css`
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
ruleUid: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
export default RuleViewer;
|
@ -4,9 +4,9 @@ import { Stack, Text } from '@grafana/ui';
|
||||
import { RuleHealth } from 'app/types/unified-alerting';
|
||||
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { AlertStateDot } from '../../AlertStateDot';
|
||||
import { AlertStateDot } from '../AlertStateDot';
|
||||
|
||||
import { isErrorHealth } from './RuleViewer.v2';
|
||||
import { isErrorHealth } from './RuleViewer';
|
||||
|
||||
interface RecordingBadgeProps {
|
||||
health?: RuleHealth;
|
@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Rule } from 'app/types/unified-alerting';
|
||||
|
||||
import { isErrorHealth } from '../rule-viewer/v2/RuleViewer.v2';
|
||||
import { isErrorHealth } from '../rule-viewer/RuleViewer';
|
||||
|
||||
interface Prom {
|
||||
rule: Rule;
|
||||
|
Loading…
Reference in New Issue
Block a user