diff --git a/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.test.tsx b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.test.tsx
index 5ba64517caf..3941bef6e6b 100644
--- a/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.test.tsx
+++ b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.test.tsx
@@ -6,9 +6,15 @@ import {
useRestrictedGrafanaApis,
} from './RestrictedGrafanaApis';
+// Mock schema for testing
+const mockAlertRuleFormSchema = {
+ parse: jest.fn((data: unknown) => data),
+ safeParse: jest.fn((data: unknown) => ({ success: true, data })),
+};
+
describe('RestrictedGrafanaApis', () => {
const apis: RestrictedGrafanaApisContextType = {
- addPanel: () => {},
+ alertingAlertRuleFormSchema: mockAlertRuleFormSchema,
};
beforeEach(() => {
@@ -21,16 +27,15 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.current.addPanel).toEqual(apis.addPanel);
- expect(Object.keys(result.current)).toEqual(['addPanel']);
+ expect(result.current.alertingAlertRuleFormSchema).toEqual(apis.alertingAlertRuleFormSchema);
+ expect(Object.keys(result.current)).toEqual(['alertingAlertRuleFormSchema']);
});
it('should share an API if the plugin is allowed using a regexp', () => {
@@ -39,16 +44,15 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.current.addPanel).toEqual(apis.addPanel);
- expect(Object.keys(result.current)).toEqual(['addPanel']);
+ expect(result.current.alertingAlertRuleFormSchema).toEqual(apis.alertingAlertRuleFormSchema);
+ expect(Object.keys(result.current)).toEqual(['alertingAlertRuleFormSchema']);
});
it('should not share an API if the plugin is not directly allowed and no allow regexp matches it', () => {
@@ -57,15 +61,14 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.current.addPanel).not.toBeDefined();
+ expect(result.current.alertingAlertRuleFormSchema).not.toBeDefined();
});
// Ideally the `allowList` and the `blockList` are not used together
@@ -75,17 +78,16 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.current.addPanel).toEqual(apis.addPanel);
- expect(Object.keys(result.current)).toEqual(['addPanel']);
+ expect(result.current.alertingAlertRuleFormSchema).toEqual(apis.alertingAlertRuleFormSchema);
+ expect(Object.keys(result.current)).toEqual(['alertingAlertRuleFormSchema']);
});
it('should share an API with allowed plugins (testing multiple plugins)', () => {
@@ -97,14 +99,13 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.result.current.addPanel).toEqual(apis.addPanel);
+ expect(result.result.current.alertingAlertRuleFormSchema).toEqual(apis.alertingAlertRuleFormSchema);
// 2. Second app
result = renderHook(() => useRestrictedGrafanaApis(), {
@@ -112,14 +113,13 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.result.current.addPanel).toEqual(apis.addPanel);
+ expect(result.result.current.alertingAlertRuleFormSchema).toEqual(apis.alertingAlertRuleFormSchema);
});
it('should not share APIs with plugins that are not allowed', () => {
@@ -128,15 +128,14 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.current.addPanel).not.toBeDefined();
+ expect(result.current.alertingAlertRuleFormSchema).not.toBeDefined();
});
it('should not share APIs with anyone if both the allowList and the blockList are empty', () => {
@@ -144,13 +143,16 @@ describe('RestrictedGrafanaApis', () => {
result = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
-
+
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.result.current.addPanel).not.toBeDefined();
+ expect(result.result.current.alertingAlertRuleFormSchema).not.toBeDefined();
result = renderHook(() => useRestrictedGrafanaApis(), {
wrapper: ({ children }: { children: React.ReactNode }) => (
@@ -159,8 +161,7 @@ describe('RestrictedGrafanaApis', () => {
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.result.current.addPanel).not.toBeDefined();
+ expect(result.result.current.alertingAlertRuleFormSchema).not.toBeDefined();
});
it('should not share APIs with blocked plugins', () => {
@@ -169,14 +170,13 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.current.addPanel).not.toBeDefined();
+ expect(result.current.alertingAlertRuleFormSchema).not.toBeDefined();
});
it('should not share APIs with plugins that match any block list regexes', () => {
@@ -185,13 +185,12 @@ describe('RestrictedGrafanaApis', () => {
{children}
),
});
- // @ts-expect-error No APIs are defined yet
- expect(result.current.addPanel).not.toBeDefined();
+ expect(result.current.alertingAlertRuleFormSchema).not.toBeDefined();
});
});
diff --git a/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx
index cfbc20eed4e..5dff77ccb03 100644
--- a/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx
+++ b/packages/grafana-data/src/context/plugins/RestrictedGrafanaApis.tsx
@@ -1,9 +1,16 @@
import { createContext, ReactElement, PropsWithChildren, useMemo, useContext } from 'react';
+// Generic schema type to avoid zod dependency in @grafana/data
+interface ZodSchema {
+ parse: (data: unknown) => unknown;
+ safeParse: (data: unknown) => { success: boolean; data?: unknown; error?: unknown };
+}
+
export interface RestrictedGrafanaApisContextTypeInternal {
// Add types for restricted Grafana APIs here
// (Make sure that they are typed as optional properties)
// e.g. addPanel?: (vizPanel: VizPanel) => void;
+ alertingAlertRuleFormSchema?: ZodSchema;
}
// We are exposing this through a "type validation", to make sure that all APIs are optional (which helps plugins catering for scenarios when they are not available).
diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.ts
index 3071b6a75c6..064fd6c93e3 100644
--- a/public/app/features/alerting/unified/rule-editor/formDefaults.ts
+++ b/public/app/features/alerting/unified/rule-editor/formDefaults.ts
@@ -1,7 +1,8 @@
import { clamp } from 'lodash';
-import { z } from 'zod';
+import z from 'zod';
import { config, getDataSourceSrv } from '@grafana/runtime';
+import { alertingAlertRuleFormSchema } from 'app/features/plugins/components/restrictedGrafanaApis/alerting/alertRuleFormSchema';
import { RuleWithLocation } from 'app/types/unified-alerting';
import { GrafanaAlertStateDecision, RulerRuleDTO } from 'app/types/unified-alerting-dto';
@@ -156,47 +157,11 @@ export function formValuesFromQueryParams(ruleDefinition: string, type: RuleForm
)
);
}
+// schema for cloud rule form values. This is necessary because the cloud rule form values are not the same as the grafana rule form values.
+// schema for grafana rule values is navigateToAlertFormSchema , shared in the restrictedGrafanaApis.
+// TODO: add this to the DMA new plugin.
-export function formValuesFromPrefill(rule: Partial): RuleFormValues {
- // coerce prefill params to a valid RuleFormValues interface
- const parsedRule = ruleFormValuesSchema.parse(rule);
-
- return revealHiddenQueries({
- ...getDefaultFormValues(rule.type),
- ...parsedRule,
- });
-}
-
-export function formValuesFromExistingRule(rule: RuleWithLocation) {
- return revealHiddenQueries(rulerRuleToFormValues(rule));
-}
-
-export function defaultFormValuesForRuleType(ruleType: RuleFormType): RuleFormValues {
- return {
- ...getDefaultFormValues(ruleType),
- condition: 'C',
- queries: getDefaultQueries(isGrafanaRecordingRuleByType(ruleType)),
- type: ruleType,
- evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL,
- };
-}
-
-// TODO This function is not 100% valid. There is no support for cloud form type because
-// it's not valid from the path param point of view.
-export function translateRouteParamToRuleType(param = ''): RuleFormType {
- if (param === 'recording') {
- return RuleFormType.cloudRecording;
- }
-
- if (param === 'grafana-recording') {
- return RuleFormType.grafanaRecording;
- }
-
- return RuleFormType.grafana;
-}
-
-// we use this schema to coerce prefilled query params into a valid "FormValues" interface
-const ruleFormValuesSchema = z.looseObject({
+const cloudRuleFormValuesSchema = z.looseObject({
name: z.string().optional(),
type: z.enum(RuleFormType).catch(RuleFormType.grafana),
dataSourceName: z.string().optional().default(''),
@@ -273,3 +238,49 @@ const ruleFormValuesSchema = z.looseObject({
expression: z.string().optional(),
missingSeriesEvalsToResolve: z.number().optional(),
});
+
+export function formValuesFromPrefill(rule: Partial): RuleFormValues {
+ let parsedRule: z.infer | z.infer;
+ // differencitate between cloud and grafana prefill
+ if (rule.type === RuleFormType.cloudAlerting) {
+ // we use this schema to coerce prefilled query params into a valid "FormValues" interface
+ parsedRule = cloudRuleFormValuesSchema.parse(rule);
+ } else {
+ // grafana prefill
+ // coerce prefill params to a valid RuleFormValues interface
+ parsedRule = alertingAlertRuleFormSchema.parse(rule);
+ }
+
+ return revealHiddenQueries({
+ ...getDefaultFormValues(rule.type),
+ ...parsedRule,
+ });
+}
+
+export function formValuesFromExistingRule(rule: RuleWithLocation) {
+ return revealHiddenQueries(rulerRuleToFormValues(rule));
+}
+
+export function defaultFormValuesForRuleType(ruleType: RuleFormType): RuleFormValues {
+ return {
+ ...getDefaultFormValues(ruleType),
+ condition: 'C',
+ queries: getDefaultQueries(isGrafanaRecordingRuleByType(ruleType)),
+ type: ruleType,
+ evaluateEvery: DEFAULT_GROUP_EVALUATION_INTERVAL,
+ };
+}
+
+// TODO This function is not 100% valid. There is no support for cloud form type because
+// it's not valid from the path param point of view.
+export function translateRouteParamToRuleType(param = ''): RuleFormType {
+ if (param === 'recording') {
+ return RuleFormType.cloudRecording;
+ }
+
+ if (param === 'grafana-recording') {
+ return RuleFormType.grafanaRecording;
+ }
+
+ return RuleFormType.grafana;
+}
diff --git a/public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx b/public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx
index 3ecbd8f95c0..4b9caec649f 100644
--- a/public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx
+++ b/public/app/features/plugins/components/restrictedGrafanaApis/RestrictedGrafanaApisProvider.tsx
@@ -2,11 +2,13 @@ import { PropsWithChildren, ReactElement } from 'react';
import { RestrictedGrafanaApisContextProvider, RestrictedGrafanaApisContextType } from '@grafana/data';
import { config } from '@grafana/runtime';
+import { alertingAlertRuleFormSchemaApi } from 'app/features/plugins/components/restrictedGrafanaApis/alerting/alertRuleFormSchema';
const restrictedGrafanaApis: RestrictedGrafanaApisContextType = config.featureToggles.restrictedPluginApis
? {
// Add your restricted APIs here
// (APIs that should be availble to ALL plugins should be shared via our packages, e.g. @grafana/data.)
+ alertingAlertRuleFormSchema: alertingAlertRuleFormSchemaApi.alertingAlertRuleFormSchema,
}
: {};
diff --git a/public/app/features/plugins/components/restrictedGrafanaApis/alerting/README.md b/public/app/features/plugins/components/restrictedGrafanaApis/alerting/README.md
new file mode 100644
index 00000000000..eab43d6cd8a
--- /dev/null
+++ b/public/app/features/plugins/components/restrictedGrafanaApis/alerting/README.md
@@ -0,0 +1,50 @@
+## Navigate to Alert Form Schema API
+
+The `alertingAlertRuleFormSchema` API provides a Zod schema for navigating to the Grafana alert form with pre-filled data. This API is useful for plugins that need to validate data before navigating to the alert creation form.
+
+### Available Schema
+
+- `alertingAlertRuleFormSchema` - Schema for data used to navigate to the alert form with pre-filled values
+
+### Usage Example
+
+```ts
+import { useRestrictedGrafanaApis } from "@grafana/data";
+
+function MyAlertingPlugin() {
+ const { alertingAlertRuleFormSchema } = useRestrictedGrafanaApis();
+
+ const validateAndNavigateToAlertForm = (data: unknown) => {
+ if (!alertingAlertRuleFormSchema) {
+ console.warn('Navigate to alert form schema API not available');
+ return;
+ }
+
+ // Validate using the navigate to alert form schema
+ const result = alertingAlertRuleFormSchema.safeParse(data);
+
+ if (result.success) {
+ console.log('Valid navigation data:', result.data);
+ // Proceed with navigating to the alert form
+ } else {
+ console.error('Validation failed:', result.error.errors);
+ }
+ };
+
+ return (
+
+
+
+ );
+}
+```
+
+### Configuration
+
+To enable the navigate to alert form schema API for specific plugins, add the following to your Grafana configuration:
+
+```ini
+[plugins.restricted_apis_allowlist]
+# Allow specific plugins to access the navigate to alert form schema API
+alertingAlertRuleFormSchema = "myorg-alerting-plugin, grafana-enterprise-.*"
+```
diff --git a/public/app/features/plugins/components/restrictedGrafanaApis/alerting/alertRuleFormSchema.ts b/public/app/features/plugins/components/restrictedGrafanaApis/alerting/alertRuleFormSchema.ts
new file mode 100644
index 00000000000..4470c195544
--- /dev/null
+++ b/public/app/features/plugins/components/restrictedGrafanaApis/alerting/alertRuleFormSchema.ts
@@ -0,0 +1,203 @@
+import { z } from 'zod';
+
+import alertDef from 'app/features/alerting/state/alertDef';
+import { RuleFormType } from 'app/features/alerting/unified/types/rule-form';
+import { ExpressionQueryType } from 'app/features/expressions/types';
+import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
+
+// Schema for __expr__ type queries (reduce, threshold, etc.)
+export const exprQuerySchema = z.object({
+ refId: z.string().describe('Reference ID for the query, e.g., "B", "C", etc.'),
+ type: z.enum(ExpressionQueryType).describe('Expression type'),
+ datasource: z.object({
+ uid: z.literal('__expr__').describe('Must be "__expr__" for expression queries'),
+ type: z.literal('__expr__').describe('Must be "__expr__" for expression queries'),
+ }),
+ conditions: z
+ .array(
+ z.object({
+ type: z.string().describe('Condition type, e.g., "query"'),
+ evaluator: z.object({
+ params: z.array(z.any()).describe('Parameters for the evaluator'),
+ type: z.enum(alertDef.evalFunctions.map((ef) => ef.value)).describe('Evaluator type'),
+ }),
+ operator: z.object({
+ type: z.enum(alertDef.evalOperators.map((eo) => eo.value)).describe('Operator type'),
+ }),
+ query: z.object({
+ params: z.array(z.string()).describe('Query parameters, typically the refId to evaluate'),
+ }),
+ reducer: z.object({
+ params: z.array(z.any()).describe('Parameters for the reducer'),
+ type: z.string().describe('Reducer type, e.g., "last", "avg", "sum", "count", "min", "max"'),
+ }),
+ })
+ )
+ .optional()
+ .describe('Conditions for the expression query'),
+ reducer: z.string().optional().describe('Reducer function, e.g., "last", "avg", "sum"'),
+ expression: z.string().optional().describe('Expression referencing other queries, e.g., "A"'),
+ math: z.string().optional().describe('Math expression for math type queries'),
+});
+
+// Schema for regular datasource queries
+export const alertingQuerySchema = z.object({
+ refId: z.string().describe('Reference ID for the query, e.g., "A", "B", etc.'),
+ queryType: z.string().optional().default('alerting').describe('Type of query (e.g., "alerting", "recording")'),
+ expression: z
+ .string()
+ .optional()
+ .default('')
+ .describe('Query expression to be executed. This can not include variables (e.g. $var).'),
+ instant: z.boolean().optional().default(true).describe('Whether the query is an instant query'),
+ range: z
+ .boolean()
+ .optional()
+ .default(false)
+ .describe('Whether the query is a range query, should be false if instant is true'),
+ datasource: z.object({
+ type: z.string().optional().describe('Datasource type or "__expr__" when it is an expression query'),
+ uid: z.string().optional().describe('Datasource UID'),
+ }),
+});
+
+// Combined schema that supports both regular and expression queries
+export const alertingModelSchema = z.union([alertingQuerySchema, exprQuerySchema]);
+
+// Main navigate to alert form schema - merged from both alertingSchemaApi and formDefaults
+export const alertingAlertRuleFormSchema = z.object({
+ // Common fields
+ name: z.string().optional().describe('Name of the alert rule'),
+ type: z.enum(RuleFormType).optional().catch(RuleFormType.grafana).describe('Type of the alert rule'),
+ dataSourceName: z.string().optional().default(''),
+ group: z.string().optional().describe('Alert group name'),
+
+ // Labels and annotations
+ labels: z
+ .array(
+ z.object({
+ key: z.string().describe('Label key'),
+ value: z.string().describe('Label value'),
+ })
+ )
+ .optional()
+ .default([])
+ .describe('Labels for the alert rule'),
+ annotations: z
+ .array(
+ z.object({
+ key: z
+ .string()
+ .describe('Annotation key; for dashboard panel annotations, use "__dashboardUid__" or "__panelId__"'),
+ value: z.string().describe('Annotation value'),
+ })
+ )
+ .optional().describe(`Optional annotations for the alert rule. When creating alerts from a dashboard panel, include:
+ - {"key": "__dashboardUid__", "value": ""}
+ - {"key": "__panelId__", "value": ""}
+ These annotations link the alert back to the source dashboard and panel.`),
+
+ // Folder configuration
+ folder: z
+ .object({
+ kind: z.enum(['folder']).default('folder'),
+ uid: z.string().describe('Folder UID where the alert rule will be created'),
+ title: z.string().optional().default('').describe('Folder title'),
+ })
+ .optional()
+ .describe('Folder configuration for the alert rule'),
+
+ // Queries
+ queries: z
+ .array(
+ z.object({
+ refId: z.string().describe('Reference ID for the query (e.g., "A", "B", "C")'),
+ queryType: z.string().optional().default('instant').describe('Type of query (e.g., "instant")'),
+ relativeTimeRange: z
+ .object({
+ from: z.number().describe('Relative time from in seconds (e.g., 3600 for 1 hour)'),
+ to: z.number().default(0).describe('Relative time to in seconds (usually 0 for "now")'),
+ })
+ .optional(),
+ datasourceUid: z.string().describe('Datasource UID for the query'),
+ model: alertingModelSchema.describe('Query model containing the actual query configuration'),
+ })
+ )
+ .optional()
+ .default([])
+ .describe('Array of queries that form the alert rule'),
+
+ // Alert rule configuration
+ condition: z.string().optional().describe('Reference ID of the query that acts as the condition (e.g., "C")'),
+ noDataState: z.enum(GrafanaAlertStateDecision).optional().describe('State when no data is available'),
+ execErrState: z.enum(GrafanaAlertStateDecision).optional().describe('State when there is an execution error'),
+ evaluateEvery: z.string().optional().describe('Evaluation interval'),
+ evaluateFor: z.string().optional().describe('Evaluation duration'),
+ keepFiringFor: z.string().optional().describe('Keep firing duration'),
+ isPaused: z.boolean().optional().default(false).describe('Whether the rule is paused'),
+
+ // Manual routing and contact points
+ manualRouting: z
+ .boolean()
+ .optional()
+ .default(true)
+ .describe('Whether to use manual routing. If true, contactPoints are used.'),
+ contactPoints: z
+ .record(
+ z.string(),
+ z.object({
+ selectedContactPoint: z.string().describe('Selected contact point to send the alert to'),
+ overrideGrouping: z.boolean().describe('Whether to override the default grouping'),
+ groupBy: z.array(z.string()).describe('Group by labels'),
+ overrideTimings: z.boolean().describe('Whether to override the default timings'),
+ groupWaitValue: z.string().describe('Group wait value'),
+ groupIntervalValue: z.string().describe('Group interval value'),
+ repeatIntervalValue: z.string().describe('Repeat interval value'),
+ muteTimeIntervals: z.array(z.string()).describe('Mute time intervals'),
+ activeTimeIntervals: z.array(z.string()).describe('Active time intervals'),
+ })
+ )
+ .optional()
+ .default({
+ GRAFANA_RULES_SOURCE_NAME: {
+ selectedContactPoint: 'default',
+ overrideGrouping: false,
+ groupBy: [],
+ overrideTimings: false,
+ groupWaitValue: '',
+ groupIntervalValue: '',
+ repeatIntervalValue: '',
+ muteTimeIntervals: [],
+ activeTimeIntervals: [],
+ },
+ })
+ .describe('Contact points configuration'),
+
+ // Editor settings
+ editorSettings: z
+ .object({
+ simplifiedQueryEditor: z.boolean(),
+ simplifiedNotificationEditor: z.boolean(),
+ })
+ .optional()
+ .default({ simplifiedQueryEditor: true, simplifiedNotificationEditor: true })
+ .describe('Editor settings'),
+
+ // Additional fields
+ metric: z.string().optional().describe('Metric name for Grafana recording rules'),
+ targetDatasourceUid: z.string().optional().describe('Target datasource UID for Grafana recording rules'),
+
+ // Navigation
+ returnTo: z.string().optional().describe('Optional URL to return to after creating the alert'),
+});
+
+// Export types for use in plugins
+export type AlertingAlertRuleFormSchemaType = z.infer;
+export type AlertingQuerySchemaType = z.infer;
+export type ExprQuerySchemaType = z.infer;
+export type AlertingModelSchemaType = z.infer;
+
+// Simple API that only exposes the navigate to alert rule form schema
+export const alertingAlertRuleFormSchemaApi = {
+ alertingAlertRuleFormSchema,
+};