diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx
index ea1a6f90859..c10c10065fd 100644
--- a/public/app/features/alerting/routes.tsx
+++ b/public/app/features/alerting/routes.tsx
@@ -225,7 +225,7 @@ const unifiedRoutes: RouteDescriptor[] = [
),
},
{
- path: '/alerting/new',
+ path: '/alerting/new/:type?',
pageClass: 'page-alerting',
roles: evaluateAccess(
[AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite],
diff --git a/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx b/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx
new file mode 100644
index 00000000000..e4e7bb6481d
--- /dev/null
+++ b/public/app/features/alerting/unified/MoreActionsRuleButtons.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { useLocation } from 'react-router-dom';
+
+import { urlUtil } from '@grafana/data';
+import { Button, Dropdown, Icon, LinkButton, Menu, MenuItem } from '@grafana/ui';
+
+import { logInfo, LogMessages } from './Analytics';
+import { useRulesAccess } from './utils/accessControlHooks';
+import { createUrl } from './utils/url';
+
+interface Props {}
+
+export function MoreActionsRuleButtons({}: Props) {
+ const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess();
+ const location = useLocation();
+ const newMenu = (
+
+ );
+
+ return (
+ <>
+ {(canCreateGrafanaRules || canCreateCloudRules) && (
+ logInfo(LogMessages.alertRuleFromScratch)}
+ >
+ New alert rule
+
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx
index 7648fd4967e..39f92cb129d 100644
--- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx
+++ b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx
@@ -127,10 +127,9 @@ describe('RuleEditor recording rules', () => {
},
});
- renderRuleEditor();
+ renderRuleEditor(undefined, true);
await waitForElementToBeRemoved(screen.getAllByTestId('Spinner'));
await userEvent.type(await ui.inputs.name.find(), 'my great new recording rule');
- await userEvent.click(await ui.buttons.lotexRecordingRule.get());
const dataSourceSelect = ui.inputs.dataSource.get();
await userEvent.click(byRole('combobox').get(dataSourceSelect));
diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx
index e3c4878df2d..d191bdb743b 100644
--- a/public/app/features/alerting/unified/RuleList.test.tsx
+++ b/public/app/features/alerting/unified/RuleList.test.tsx
@@ -117,7 +117,8 @@ const ui = {
rulesFilterInput: byTestId('search-query-input'),
moreErrorsButton: byRole('button', { name: /more errors/ }),
editCloudGroupIcon: byTestId('edit-group'),
- newRuleButton: byRole('link', { name: 'Create alert rule' }),
+ newRuleButton: byRole('link', { name: 'New alert rule' }),
+ moreButton: byRole('button', { name: 'More' }),
exportButton: byRole('link', { name: /export/i }),
editGroupModal: {
dialog: byRole('dialog'),
@@ -697,11 +698,14 @@ describe('RuleList', () => {
renderRuleList();
+ await userEvent.click(ui.moreButton.get());
expect(ui.exportButton.get()).toBeInTheDocument();
});
it('Export button should not be visible when the user has no alert provisioning read permissions', async () => {
enableRBAC();
+ grantUserPermissions([AccessControlAction.AlertingRuleCreate, AccessControlAction.FoldersRead]);
+
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));
mocks.api.fetchRules.mockResolvedValue([]);
@@ -709,6 +713,7 @@ describe('RuleList', () => {
renderRuleList();
+ await userEvent.click(ui.moreButton.get());
expect(ui.exportButton.query()).not.toBeInTheDocument();
});
});
@@ -831,7 +836,7 @@ describe('RuleList', () => {
await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1));
- const button = screen.getByText('Create alert rule');
+ const button = screen.getByText('New alert rule');
button.addEventListener('click', (event) => event.preventDefault(), false);
diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx
index 4a6d807b44b..6aaaec90e1d 100644
--- a/public/app/features/alerting/unified/RuleList.tsx
+++ b/public/app/features/alerting/unified/RuleList.tsx
@@ -1,18 +1,17 @@
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, urlUtil } from '@grafana/data';
+import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
-import { logInfo } from '@grafana/runtime';
-import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui';
+import { Button, useStyles2, withErrorBoundary } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { useDispatch } from 'app/types';
import { CombinedRuleNamespace } from '../../../types/unified-alerting';
-import { LogMessages, trackRuleListNavigation } from './Analytics';
+import { trackRuleListNavigation } from './Analytics';
+import { MoreActionsRuleButtons } from './MoreActionsRuleButtons';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { NoRulesSplash } from './components/rules/NoRulesCTA';
import { INSTANCES_DISPLAY_LIMIT } from './components/rules/RuleDetails';
@@ -28,7 +27,6 @@ import { fetchAllPromAndRulerRulesAction } from './state/actions';
import { useRulesAccess } from './utils/accessControlHooks';
import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants';
import { getAllRulesSourceNames } from './utils/datasource';
-import { createUrl } from './utils/url';
const VIEWS = {
groups: RuleListGroupView,
@@ -43,7 +41,6 @@ const RuleList = withErrorBoundary(
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
- const location = useLocation();
const [expandAll, setExpandAll] = useState(false);
const onFilterCleared = useCallback(() => setExpandAll(false), []);
@@ -51,8 +48,6 @@ const RuleList = withErrorBoundary(
const [queryParams] = useQueryParams();
const { filterState, hasActiveFilters } = useRulesFilter();
- const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess();
-
const view = VIEWS[queryParams['view'] as keyof typeof VIEWS]
? (queryParams['view'] as keyof typeof VIEWS)
: 'groups';
@@ -96,6 +91,8 @@ const RuleList = withErrorBoundary(
const combinedNamespaces: CombinedRuleNamespace[] = useCombinedRuleNamespaces();
const filteredNamespaces = useFilteredRules(combinedNamespaces, filterState);
+ const { canCreateGrafanaRules, canCreateCloudRules, canReadProvisioning } = useRulesAccess();
+
return (
// We don't want to show the Loading... indicator for the whole page.
// We show separate indicators for Grafana-managed and Cloud rules
@@ -119,31 +116,11 @@ const RuleList = withErrorBoundary(
)}
-
- {canReadProvisioning && (
-
- Export
-
- )}
- {(canCreateGrafanaRules || canCreateCloudRules) && (
- logInfo(LogMessages.alertRuleFromScratch)}
- >
- Create alert rule
-
- )}
-
+ {(canCreateGrafanaRules || canCreateCloudRules || canReadProvisioning) && (
+
+
+
+ )}
>
)}
diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx
index aee899e9964..deb450c5c7c 100644
--- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react';
import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
-import { Link } from 'react-router-dom';
+import { Link, useParams } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { config, logInfo } from '@grafana/runtime';
@@ -28,6 +28,7 @@ import { NotificationsStep } from './NotificationsStep';
import { RuleEditorSection } from './RuleEditorSection';
import { RuleInspector } from './RuleInspector';
import { QueryAndExpressionsStep } from './query-and-alert-condition/QueryAndExpressionsStep';
+import { translateRouteParamToRuleType } from './util';
const recordingRuleNameValidationPattern = {
message:
@@ -81,6 +82,9 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
const [showEditYaml, setShowEditYaml] = useState(false);
const [evaluateEvery, setEvaluateEvery] = useState(existing?.group.interval ?? MINUTE);
+ const routeParams = useParams<{ type: string }>();
+ const ruleType = translateRouteParamToRuleType(routeParams.type);
+
const returnTo: string = (queryParams['returnTo'] as string | undefined) ?? '/alerting/list';
const [showDeleteModal, setShowDeleteModal] = useState(false);
@@ -101,10 +105,10 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
queries: getDefaultQueries(),
condition: 'C',
...(queryParams['defaults'] ? JSON.parse(queryParams['defaults'] as string) : {}),
- type: RuleFormType.grafana,
+ type: ruleType || RuleFormType.grafana,
evaluateEvery: evaluateEvery,
};
- }, [existing, prefill, queryParams, evaluateEvery]);
+ }, [existing, prefill, queryParams, evaluateEvery, ruleType]);
const formAPI = useForm({
mode: 'onSubmit',
diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx
index dd1b173c071..14041e827bb 100644
--- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.test.tsx
@@ -2,8 +2,10 @@ import { render } from '@testing-library/react';
import React from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { Provider } from 'react-redux';
+import { Router } from 'react-router-dom';
import { byText } from 'testing-library-selector';
+import { locationService } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction } from 'app/types';
@@ -28,7 +30,9 @@ function renderAlertTypeStep() {
render(
-
+
+
+
,
{ wrapper: FormProviderWrapper }
);
@@ -36,7 +40,7 @@ function renderAlertTypeStep() {
describe('RuleTypePicker', () => {
describe('RBAC', () => {
- it('Should display grafana, mimir alert and mimir recording buttons when user has rule create and write permissions', async () => {
+ it('Should display grafana and mimir alert when user has rule create and write permissions', async () => {
jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => {
return [AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite].includes(
action as AccessControlAction
@@ -47,7 +51,17 @@ describe('RuleTypePicker', () => {
expect(ui.ruleTypePicker.grafanaManagedButton.get()).toBeInTheDocument();
expect(ui.ruleTypePicker.mimirOrLokiButton.get()).toBeInTheDocument();
- expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.get()).toBeInTheDocument();
+ });
+
+ it('Should not display the recording rule button', async () => {
+ jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => {
+ return [AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite].includes(
+ action as AccessControlAction
+ );
+ });
+
+ renderAlertTypeStep();
+ expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.query()).not.toBeInTheDocument();
});
it('Should hide grafana button when user does not have rule create permission', () => {
@@ -59,7 +73,7 @@ describe('RuleTypePicker', () => {
expect(ui.ruleTypePicker.grafanaManagedButton.query()).not.toBeInTheDocument();
expect(ui.ruleTypePicker.mimirOrLokiButton.get()).toBeInTheDocument();
- expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.get()).toBeInTheDocument();
+ expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.query()).not.toBeInTheDocument();
});
it('Should hide mimir alert and mimir recording when user does not have rule external write permission', () => {
diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.tsx
index 49a75f973fe..a11a2884077 100644
--- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/AlertType.tsx
@@ -31,7 +31,7 @@ export const AlertType = ({ editingExistingRule }: Props) => {
return (
<>
- {!editingExistingRule && (
+ {!editingExistingRule && ruleFormType !== RuleFormType.cloudRecording && (
(
diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx
index 235a8681844..f067b445d5f 100644
--- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx
@@ -186,6 +186,11 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P
clearPreviewData();
if (type === RuleFormType.cloudRecording) {
const expr = getValues('expression');
+
+ if (!recordingRuleDefaultDatasource) {
+ return;
+ }
+
const datasourceUid =
(editingExistingRule && getDataSourceSrv().getInstanceSettings(dataSourceName)?.uid) ||
recordingRuleDefaultDatasource.uid;
diff --git a/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx b/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx
index b0ddea16b8b..9f51d74e4f1 100644
--- a/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/rule-types/RuleTypePicker.tsx
@@ -13,8 +13,6 @@ import { RuleFormType } from '../../../types/rule-form';
import { GrafanaManagedRuleType } from './GrafanaManagedAlert';
import { MimirFlavoredType } from './MimirOrLokiAlert';
-import { RecordingRuleType } from './MimirOrLokiRecordingRule';
-
interface RuleTypePickerProps {
onChange: (value: RuleFormType) => void;
selected: RuleFormType;
@@ -31,23 +29,20 @@ const RuleTypePicker = ({ selected, onChange, enabledTypes }: RuleTypePickerProp
const styles = useStyles2(getStyles);
+ const handleChange = (type: RuleFormType) => {
+ onChange(type);
+ };
+
return (
<>
{enabledTypes.includes(RuleFormType.grafana) && (
-
+
)}
{enabledTypes.includes(RuleFormType.cloudAlerting) && (
- )}
- {enabledTypes.includes(RuleFormType.cloudRecording) && (
-
)}
diff --git a/public/app/features/alerting/unified/components/rule-editor/util.ts b/public/app/features/alerting/unified/components/rule-editor/util.ts
index 626551539af..b1161f5a1f1 100644
--- a/public/app/features/alerting/unified/components/rule-editor/util.ts
+++ b/public/app/features/alerting/unified/components/rule-editor/util.ts
@@ -8,6 +8,8 @@ import { isExpressionQuery } from 'app/features/expressions/guards';
import { ClassicCondition, ExpressionQueryType } from 'app/features/expressions/types';
import { AlertQuery } from 'app/types/unified-alerting-dto';
+import { RuleFormType } from '../../types/rule-form';
+
import { createDagFromQueries, getOriginOfRefId } from './dag';
export function queriesWithUpdatedReferences(
@@ -293,3 +295,11 @@ export function getStatusMessage(data: PanelData): string | undefined {
return data.error?.message ?? genericErrorMessage;
}
+
+export function translateRouteParamToRuleType(param = ''): RuleFormType {
+ if (param === 'recording') {
+ return RuleFormType.cloudRecording;
+ }
+
+ return RuleFormType.grafana;
+}
diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx
index 8f3d1d52916..530b97f6eb4 100644
--- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx
+++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx
@@ -15,8 +15,8 @@ export const NoRulesSplash = () => {
-
+
);
}