From 336acaf0bf468f0327d2e51f54497f7bd0d86a3f Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 14 Mar 2024 15:18:01 +0100 Subject: [PATCH] Alerting: Promote new alerting detail view (#84277) --- .betterer.results | 15 - .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 8 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 3 +- .../features/alerting/unified/RuleViewer.tsx | 29 +- .../rule-viewer/{v2 => }/Actions.tsx | 12 +- .../rule-viewer/{v2 => }/DeleteModal.tsx | 6 +- .../rule-viewer/{v2 => }/RuleContext.tsx | 0 ...Viewer.v2.test.tsx => RuleViewer.test.tsx} | 8 +- .../{v2/RuleViewer.v2.tsx => RuleViewer.tsx} | 34 +- .../rule-viewer/RuleViewer.v1.test.tsx | 378 ------------------ .../components/rule-viewer/RuleViewer.v1.tsx | 262 ------------ .../rule-viewer/{v2 => }/StateBadges.tsx | 4 +- .../rule-viewer/{v2 => }/__mocks__/server.ts | 0 .../unified/components/rules/RuleHealth.tsx | 2 +- 17 files changed, 44 insertions(+), 723 deletions(-) rename public/app/features/alerting/unified/components/rule-viewer/{v2 => }/Actions.tsx (91%) rename public/app/features/alerting/unified/components/rule-viewer/{v2 => }/DeleteModal.tsx (90%) rename public/app/features/alerting/unified/components/rule-viewer/{v2 => }/RuleContext.tsx (100%) rename public/app/features/alerting/unified/components/rule-viewer/{v2/RuleViewer.v2.test.tsx => RuleViewer.test.tsx} (97%) rename public/app/features/alerting/unified/components/rule-viewer/{v2/RuleViewer.v2.tsx => RuleViewer.tsx} (91%) delete mode 100644 public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.test.tsx delete mode 100644 public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx rename public/app/features/alerting/unified/components/rule-viewer/{v2 => }/StateBadges.tsx (94%) rename public/app/features/alerting/unified/components/rule-viewer/{v2 => }/__mocks__/server.ts (100%) diff --git a/.betterer.results b/.betterer.results index 7ca5ed674b6..cc85cfbc333 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 7005c830c44..6cad16e5de5 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -152,7 +152,6 @@ export interface FeatureToggles { canvasPanelPanZoom?: boolean; logsInfiniteScrolling?: boolean; flameGraphItemCollapsing?: boolean; - alertingDetailsViewV2?: boolean; datatrails?: boolean; alertingSimplifiedRouting?: boolean; logRowsPopoverMenu?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 92ba54685aa..f39e1d31f70 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index dc766a2a79d..9e24d3b1dde 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a78d806b0c0..f41ca9d7817 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 4202850a8ac..f0816342489 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/public/app/features/alerting/unified/RuleViewer.tsx b/public/app/features/alerting/unified/RuleViewer.tsx index 9d3112a67be..32064616fa9 100644 --- a/public/app/features/alerting/unified/RuleViewer.tsx +++ b/public/app/features/alerting/unified/RuleViewer.tsx @@ -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 ? : ; -}; - -export const defaultPageNav: NavModelItem = { - id: 'alert-rule-view', - text: '', -}; - -const RuleViewerV1Wrapper = (props: RuleViewerProps) => ; - -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 ( - + ); } @@ -82,6 +66,11 @@ const RuleViewerV2Wrapper = (props: RuleViewerProps) => { ); }; +export const defaultPageNav: NavModelItem = { + id: 'alert-rule-view', + text: '', +}; + interface ErrorMessageProps { error: unknown; } diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/Actions.tsx b/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx similarity index 91% rename from public/app/features/alerting/unified/components/rule-viewer/v2/Actions.tsx rename to public/app/features/alerting/unified/components/rule-viewer/Actions.tsx index dad6cc9c6b6..8dd22260189 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/v2/Actions.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/Actions.tsx @@ -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'; diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/DeleteModal.tsx b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx similarity index 90% rename from public/app/features/alerting/unified/components/rule-viewer/v2/DeleteModal.tsx rename to public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx index 12739291681..ef5c1cdac4a 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/v2/DeleteModal.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/DeleteModal.tsx @@ -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]; diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleContext.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleContext.tsx similarity index 100% rename from public/app/features/alerting/unified/components/rule-viewer/v2/RuleContext.tsx rename to public/app/features/alerting/unified/components/rule-viewer/RuleContext.tsx diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx similarity index 97% rename from public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.test.tsx rename to public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx index 4d2c2392e84..1a82e056194 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.test.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.test.tsx @@ -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 diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx similarity index 91% rename from public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx rename to public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index 0088ddd0833..e82a9d76eba 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/v2/RuleViewer.v2.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -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', diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.test.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.test.tsx deleted file mode 100644 index cd4c5c582a3..00000000000 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.test.tsx +++ /dev/null @@ -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( - - - - ); - - 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(); - }); - }); -}); diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx deleted file mode 100644 index d1ff060bdd7..00000000000 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.v1.tsx +++ /dev/null @@ -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 ( - - -
{errorMessage}
-
-
- ); - } - - const rulesSource = getRulesSourceByName(identifier.ruleSourceName); - - if (loading) { - return ( - - - - ); - } - - if (error || !rulesSource) { - return ( - -
- {isFetchError(error) ? error.message : errorMessage} -
- {/* TODO Fix typescript */} - {/* {error && error?.stack} */} -
-
- ); - } - - if (!rule) { - return ( - - Rule could not be found. - - ); - } - - const isFederatedRule = isFederatedRuleGroup(rule.group); - const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); - - return ( - ( - - - {rule.name} - - - )} - > - {isFederatedRule && ( - - - Federated rule groups are currently an experimental feature. - - - - )} - {isProvisioned && } - <> - -
-
- {rule.promRule && ( - - - - )} - {!!rule.labels && !!Object.keys(rule.labels).length && ( - - - - )} - - -
-
- - {isFederatedRule && } - - {decodeGrafanaNamespace(rule.namespace).name} / {rule.group.name} - - {isGrafanaRulerRule(rule.rulerRule) && } -
-
-
- - - -
- - - {expandQuery && } - -
- ); -} - -function GrafanaRuleUID({ rule }: { rule: GrafanaRuleDefinition }) { - const styles = useStyles2(getStyles); - const copyUID = () => navigator.clipboard && navigator.clipboard.writeText(rule.uid); - - return ( - - {rule.uid} - - ); -} - -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; diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/StateBadges.tsx b/public/app/features/alerting/unified/components/rule-viewer/StateBadges.tsx similarity index 94% rename from public/app/features/alerting/unified/components/rule-viewer/v2/StateBadges.tsx rename to public/app/features/alerting/unified/components/rule-viewer/StateBadges.tsx index abda0ee41e8..8c478be41de 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/v2/StateBadges.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/StateBadges.tsx @@ -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; diff --git a/public/app/features/alerting/unified/components/rule-viewer/v2/__mocks__/server.ts b/public/app/features/alerting/unified/components/rule-viewer/__mocks__/server.ts similarity index 100% rename from public/app/features/alerting/unified/components/rule-viewer/v2/__mocks__/server.ts rename to public/app/features/alerting/unified/components/rule-viewer/__mocks__/server.ts diff --git a/public/app/features/alerting/unified/components/rules/RuleHealth.tsx b/public/app/features/alerting/unified/components/rules/RuleHealth.tsx index 1b0a2625b5f..66e4e339199 100644 --- a/public/app/features/alerting/unified/components/rules/RuleHealth.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleHealth.tsx @@ -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;