Compare commits

...

1 Commits

Author SHA1 Message Date
Gilles De Mey
4a6a8f661d lazy load zod proposal
this still requires zod to be unbundled in the "scope" feature
2025-11-06 17:30:18 +01:00
6 changed files with 121 additions and 100 deletions

View File

@@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom-v5-compat';
@@ -92,11 +92,12 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) =>
const ruleType = translateRouteParamToRuleType(routeParams.type);
const defaultValues: RuleFormValues = useMemo(() => {
const defaultValues = useCallback(async () => {
// If we have an existing AND a prefill, then we're coming from the restore dialog
// and we want to merge the two
if (existing && prefill) {
return { ...formValuesFromExistingRule(existing), ...formValuesFromPrefill(prefill) };
const prefillValues = await formValuesFromPrefill(prefill);
return { ...formValuesFromExistingRule(existing), ...prefillValues };
}
if (existing) {
return formValuesFromExistingRule(existing);

View File

@@ -1,8 +1,6 @@
import { clamp } from 'lodash';
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';
@@ -157,90 +155,15 @@ 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.
// ⚠️ Lazy-loaded function to avoid bundling zod in the main bundle
// This function is async to allow dynamic imports of the zod schemas
export async function formValuesFromPrefill(rule: Partial<RuleFormValues>): Promise<RuleFormValues> {
const [{ cloudRuleFormValuesSchema }, { alertingAlertRuleFormSchema }] = await Promise.all([
import('./formDefaultsSchemas'),
import('app/features/plugins/components/restrictedGrafanaApis/alerting/alertRuleFormSchema'),
]);
const cloudRuleFormValuesSchema = z.looseObject({
name: z.string().optional(),
type: z.enum(RuleFormType).catch(RuleFormType.grafana),
dataSourceName: z.string().optional().default(''),
group: z.string().optional(),
labels: z
.array(
z.object({
key: z.string(),
value: z.string(),
})
)
.optional()
.default([]),
annotations: z
.array(
z.object({
key: z.string(),
value: z.string(),
})
)
.optional()
.default([]),
queries: z.array(z.any()).optional(),
condition: z.string().optional(),
noDataState: z
.enum(GrafanaAlertStateDecision)
.optional()
.default(GrafanaAlertStateDecision.NoData)
.catch(GrafanaAlertStateDecision.NoData),
execErrState: z
.enum(GrafanaAlertStateDecision)
.optional()
.default(GrafanaAlertStateDecision.Error)
.catch(GrafanaAlertStateDecision.Error),
folder: z
.union([
z.object({
title: z.string(),
uid: z.string(),
}),
z.undefined(),
])
.optional(),
evaluateEvery: z.string().optional(),
evaluateFor: z.string().optional().default('0s'),
keepFiringFor: z.string().optional(),
isPaused: z.boolean().optional().default(false),
manualRouting: z.boolean().optional(),
contactPoints: z
.record(
z.string(),
z.object({
selectedContactPoint: z.string(),
overrideGrouping: z.boolean(),
groupBy: z.array(z.string()),
overrideTimings: z.boolean(),
groupWaitValue: z.string(),
groupIntervalValue: z.string(),
repeatIntervalValue: z.string(),
muteTimeIntervals: z.array(z.string()),
activeTimeIntervals: z.array(z.string()),
})
)
.optional(),
editorSettings: z
.object({
simplifiedQueryEditor: z.boolean(),
simplifiedNotificationEditor: z.boolean(),
})
.optional(),
metric: z.string().optional(),
targetDatasourceUid: z.string().optional(),
namespace: z.string().optional(),
expression: z.string().optional(),
missingSeriesEvalsToResolve: z.number().optional(),
});
export function formValuesFromPrefill(rule: Partial<RuleFormValues>): RuleFormValues {
let parsedRule: z.infer<typeof alertingAlertRuleFormSchema> | z.infer<typeof cloudRuleFormValuesSchema>;
let parsedRule: Partial<RuleFormValues>;
// 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

View File

@@ -0,0 +1,87 @@
import z from 'zod';
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
import { RuleFormType } from '../types/rule-form';
// 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 const cloudRuleFormValuesSchema = z.looseObject({
name: z.string().optional(),
type: z.enum(RuleFormType).catch(RuleFormType.grafana),
dataSourceName: z.string().optional().default(''),
group: z.string().optional(),
labels: z
.array(
z.object({
key: z.string(),
value: z.string(),
})
)
.optional()
.default([]),
annotations: z
.array(
z.object({
key: z.string(),
value: z.string(),
})
)
.optional()
.default([]),
queries: z.array(z.any()).optional(),
condition: z.string().optional(),
noDataState: z
.enum(GrafanaAlertStateDecision)
.optional()
.default(GrafanaAlertStateDecision.NoData)
.catch(GrafanaAlertStateDecision.NoData),
execErrState: z
.enum(GrafanaAlertStateDecision)
.optional()
.default(GrafanaAlertStateDecision.Error)
.catch(GrafanaAlertStateDecision.Error),
folder: z
.union([
z.object({
title: z.string(),
uid: z.string(),
}),
z.undefined(),
])
.optional(),
evaluateEvery: z.string().optional(),
evaluateFor: z.string().optional().default('0s'),
keepFiringFor: z.string().optional(),
isPaused: z.boolean().optional().default(false),
manualRouting: z.boolean().optional(),
contactPoints: z
.record(
z.string(),
z.object({
selectedContactPoint: z.string(),
overrideGrouping: z.boolean(),
groupBy: z.array(z.string()),
overrideTimings: z.boolean(),
groupWaitValue: z.string(),
groupIntervalValue: z.string(),
repeatIntervalValue: z.string(),
muteTimeIntervals: z.array(z.string()),
activeTimeIntervals: z.array(z.string()),
})
)
.optional(),
editorSettings: z
.object({
simplifiedQueryEditor: z.boolean(),
simplifiedNotificationEditor: z.boolean(),
})
.optional(),
metric: z.string().optional(),
targetDatasourceUid: z.string().optional(),
namespace: z.string().optional(),
expression: z.string().optional(),
missingSeriesEvalsToResolve: z.number().optional(),
});

View File

@@ -21,7 +21,6 @@ import { notifyApp } from 'app/core/reducers/appNotification';
import { contextSrv } from 'app/core/services/context_srv';
import { getMessageFromError } from 'app/core/utils/errors';
import { getCreateAlertInMenuAvailability } from 'app/features/alerting/unified/utils/access-control';
import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
import { getTrackingSource, shareDashboardType } from 'app/features/dashboard/components/ShareModal/utils';
import { InspectTab } from 'app/features/inspector/types';
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
@@ -527,6 +526,8 @@ export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) {
const onCreateAlert = async (panel: VizPanel) => {
try {
// ⚠️ Dynamically importing this to prevent Zod from being bundled into the dashboard bundle
const { scenesPanelToRuleFormValues } = await import('app/features/alerting/unified/utils/rule-form');
const formValues = await scenesPanelToRuleFormValues(panel);
const ruleFormUrl = urlUtil.renderUrl('/alerting/new', {
defaults: JSON.stringify(formValues),

View File

@@ -7,7 +7,6 @@ import { contextSrv } from 'app/core/services/context_srv';
import { getMessageFromError } from 'app/core/utils/errors';
import { getExploreUrl } from 'app/core/utils/explore';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { panelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import {
@@ -175,6 +174,8 @@ export function getPanelMenu(
const createAlert = async () => {
let formValues: Partial<RuleFormValues> | undefined;
try {
// ⚠️ Dynamically importing this to prevent Zod from being bundled into the dashboard bundle
const { panelToRuleFormValues } = await import('app/features/alerting/unified/utils/rule-form');
formValues = await panelToRuleFormValues(panel, dashboard);
} catch (err) {
const message = `Error getting rule values from the panel: ${getMessageFromError(err)}`;

View File

@@ -1,16 +1,7 @@
import { PropsWithChildren, ReactElement } from 'react';
import { PropsWithChildren, ReactElement, useEffect, useState } 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,
}
: {};
// This Provider is a wrapper around `RestrictedGrafanaApisContextProvider` from `@grafana/data`.
// The reason for this is that like this we only need to define the configuration once (here) and can use it in multiple places (app root page, extensions).
@@ -18,6 +9,23 @@ export function RestrictedGrafanaApisProvider({
children,
pluginId,
}: PropsWithChildren<{ pluginId: string }>): ReactElement {
const [restrictedGrafanaApis, setRestrictedGrafanaApis] = useState<RestrictedGrafanaApisContextType>({});
// Add your restricted APIs here
// (APIs that should be availble to ALL plugins should be shared via our packages, e.g. @grafana/data.)
useEffect(() => {
if (!config.featureToggles.restrictedPluginApis) {
return;
}
// ⚠️ Lazy-load the alerting schema to avoid bundling zod in the main app bundle
import('app/features/plugins/components/restrictedGrafanaApis/alerting/alertRuleFormSchema').then((module) => {
setRestrictedGrafanaApis({
alertingAlertRuleFormSchema: module.alertingAlertRuleFormSchemaApi.alertingAlertRuleFormSchema,
});
});
}, []);
return (
<RestrictedGrafanaApisContextProvider
pluginId={pluginId}