Add rule definition section to panel

This commit is contained in:
laurenashleigh
2026-01-12 11:41:24 +00:00
parent 9e78e62f1e
commit 59e062d1aa
8 changed files with 312 additions and 27 deletions
@@ -0,0 +1,33 @@
import { FormProvider, useForm } from 'react-hook-form';
import { t } from '@grafana/i18n';
import { Drawer } from '@grafana/ui';
import { RuleDefinitionSection } from 'app/features/alerting/unified/components/RuleDefinitionSection';
import { getDefaultFormValues } from '../rule-editor/formDefaults';
import { RuleFormType, RuleFormValues } from '../types/rule-form';
export interface AlertRuleDrawerFormProps {
isOpen: boolean;
onClose: () => void;
title?: string;
}
export function AlertRuleDrawerForm({ isOpen, onClose, title }: AlertRuleDrawerFormProps) {
const methods = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues(RuleFormType.grafana) });
if (!isOpen) {
return null;
}
return (
<Drawer
title={title ?? t('alerting.new-rule-from-panel-button.new-alert-rule', 'New alert rule')}
onClose={onClose}
>
<FormProvider {...methods}>
<RuleDefinitionSection type={RuleFormType.grafana} />
</FormProvider>
</Drawer>
);
}
@@ -0,0 +1,129 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { Field, Input, Stack, useStyles2 } from '@grafana/ui';
import { RuleFormType, RuleFormValues } from '../types/rule-form';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { isCloudRecordingRuleByType, isGrafanaManagedRuleByType, isRecordingRuleByType } from '../utils/rules';
import { FolderSelectorV2 } from './rule-editor/FolderSelectorV2';
import { LabelsEditorModal } from './rule-editor/labels/LabelsEditorModal';
import { LabelsFieldInForm } from './rule-editor/labels/LabelsFieldInForm';
export function RuleDefinitionSection({ type }: { type: RuleFormType }) {
const styles = useStyles2(getStyles);
const {
register,
formState: { errors },
setValue,
getValues,
} = useFormContext<RuleFormValues>();
const [showLabelsEditor, setShowLabelsEditor] = useState(false);
const isRecording = isRecordingRuleByType(type);
const isCloudRecordingRule = isCloudRecordingRuleByType(type);
const namePlaceholder = isRecording ? 'recording rule' : 'alert rule';
return (
<div className={styles.section}>
<div className={styles.sectionHeaderRow}>
<span className={styles.stepBadge}>
<Trans i18nKey="alerting.simplified.step-number-one">1</Trans>
</span>
<div className={styles.sectionHeader}>
<Trans i18nKey="alerting.simplified.rule-definition">Rule Definition</Trans>
</div>
</div>
<div className={styles.contentIndented}>
<Stack direction="column" gap={2}>
<Field
noMargin
label={<Trans i18nKey="alerting.alert-rule-name-and-metric.label-name">Name</Trans>}
error={errors?.name?.message}
invalid={!!errors.name?.message}
>
<Input
data-testid={selectors.components.AlertRules.ruleNameField}
id="name"
width={38}
{...register('name', {
required: {
value: true,
message: t('alerting.alert-rule-name-and-metric.message.must-enter-a-name', 'Must enter a name'),
},
pattern: isCloudRecordingRule
? {
value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
message: t(
'alerting.alert-rule-name-and-metric.recording-rule-pattern',
'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.'
),
}
: undefined,
})}
aria-label={t('alerting.alert-rule-name-and-metric.aria-label-name', 'name')}
placeholder={t(
'alerting.alert-rule-name-and-metric.placeholder-name',
'Give your {{namePlaceholder}} a name',
{ namePlaceholder }
)}
/>
</Field>
{isGrafanaManagedRuleByType(type) && (
<>
<FolderSelectorV2 />
<LabelsFieldInForm showHelpTooltip onEditClick={() => setShowLabelsEditor(true)} labelVariant="small" />
<LabelsEditorModal
isOpen={showLabelsEditor}
onClose={(labelsToUpdate) => {
if (labelsToUpdate) {
setValue('labels', labelsToUpdate);
}
setShowLabelsEditor(false);
}}
dataSourceName={GRAFANA_RULES_SOURCE_NAME}
initialLabels={getValues('labels')}
/>
</>
)}
</Stack>
</div>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
section: css({ width: '100%' }),
sectionHeaderRow: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
marginBottom: theme.spacing(1),
}),
sectionHeader: css({
fontWeight: 600,
fontSize: theme.typography.h4.fontSize,
lineHeight: theme.typography.h4.lineHeight,
}),
stepBadge: css({
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
height: 20,
width: 20,
borderRadius: theme.shape.radius.circle,
background: theme.colors.primary.main,
color: theme.colors.text.maxContrast,
fontSize: theme.typography.bodySmall.fontSize,
fontWeight: 600,
}),
contentIndented: css({ marginLeft: `calc(20px + ${theme.spacing(1)})` }),
};
}
@@ -27,8 +27,8 @@ export const CreateNewFolder = ({ onCreate }: { onCreate: (folder: Folder) => vo
onClick={() => setIsCreatingFolder(true)}
type="button"
icon="plus"
fill="outline"
variant="secondary"
size="sm"
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
>
<Trans i18nKey="alerting.create-new-folder.new-folder">New folder</Trans>
@@ -5,12 +5,13 @@ import { useAsync } from 'react-use';
import { urlUtil } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { Alert, Button, Drawer, LinkButton } from '@grafana/ui';
import { Alert, Button, LinkButton } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { useSelector } from 'app/types/store';
import { LogMessages, logInfo } from '../../Analytics';
import { AlertRuleDrawerForm } from '../../components/AlertRuleDrawerForm';
import { panelToRuleFormValues } from '../../utils/rule-form';
interface Props {
@@ -72,18 +73,11 @@ export const NewRuleFromPanelButton = ({ dashboard, panel, className }: Props) =
icon="bell"
className={className}
data-testid="create-alert-rule-button-drawer"
onClick={() => {
setIsOpen(true);
}}
onClick={() => setIsOpen(true)}
>
<Trans i18nKey="alerting.new-rule-from-panel-button.new-alert-rule">New alert rule</Trans>
</Button>
<Drawer
title={t('alerting.new-rule-from-panel-button.new-alert-rule', 'New alert rule')}
onClose={() => setIsOpen(false)}
>
<Trans i18nKey="alerting.new-rule-from-panel-button.content-coming-soon">Content coming soon...</Trans>
</Drawer>
<AlertRuleDrawerForm isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}
@@ -0,0 +1,94 @@
import { useCallback } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
import { Field, Icon, Label, Stack, Tooltip } from '@grafana/ui';
import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
import { Folder, RuleFormValues } from '../../types/rule-form';
import { CreateNewFolder } from '../create-folder/CreateNewFolder';
export function FolderSelectorV2() {
const {
formState: { errors },
setValue,
watch,
} = useFormContext<RuleFormValues>();
const resetGroup = useCallback(() => {
setValue('group', '');
}, [setValue]);
const folder = watch('folder');
const handleFolderCreation = (folder: Folder) => {
resetGroup();
setValue('folder', folder);
};
return (
<Stack alignItems="center">
{
<Field
noMargin
label={
<Label
htmlFor="folder"
description={t(
'alerting.folder-selector.description-select-folder',
'Select a folder to store your rule in.'
)}
>
<Stack direction="row" alignItems="center" gap={0.5}>
<Trans i18nKey="alerting.rule-form.folder.label">Folder</Trans>
<Tooltip
content={t(
'alerting.rule-form.folders.help-info',
'Folders are used for storing alert rules. You can extend the access provided by a role to alert rules and assign permissions to individual folders.'
)}
>
<Icon name="info-circle" size="sm" />
</Tooltip>
</Stack>
</Label>
}
error={errors.folder?.message}
data-testid="folder-picker"
>
<Stack direction="column" alignItems="flex-start" gap={0.5}>
<Controller
render={({ field: { ref, ...field } }) => (
<div style={{ width: 420 }}>
<NestedFolderPicker
permission="view"
showRootFolder={false}
invalid={!!errors.folder?.message}
{...field}
value={folder?.uid}
onChange={(uid, title) => {
if (uid && title) {
setValue('folder', { title, uid });
} else {
setValue('folder', undefined);
}
resetGroup();
}}
/>
</div>
)}
name="folder"
rules={{
required: {
value: true,
message: t('alerting.folder-selector.message.select-a-folder', 'Select a folder'),
},
}}
/>
<CreateNewFolder onCreate={handleFolderCreation} />
</Stack>
</Field>
}
</Stack>
);
}
@@ -1,7 +1,7 @@
import { useFormContext } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
import { Button, Stack, Text } from '@grafana/ui';
import { Button, Icon, Stack, Text, Tooltip } from '@grafana/ui';
import { AIImproveLabelsButtonComponent } from '../../../enterprise-components/AI/AIGenImproveLabelsButton/addAIImproveLabelsButton';
import { RuleFormValues } from '../../../types/rule-form';
@@ -12,8 +12,14 @@ import { LabelsInRule } from './LabelsField';
interface LabelsFieldInFormProps {
onEditClick: () => void;
labelVariant?: 'small' | 'default';
showHelpTooltip?: boolean;
}
export function LabelsFieldInForm({ onEditClick }: LabelsFieldInFormProps) {
export function LabelsFieldInForm({
onEditClick,
labelVariant = 'default',
showHelpTooltip = false,
}: LabelsFieldInFormProps) {
const { watch } = useFormContext<RuleFormValues>();
const labels = watch('labels');
@@ -35,7 +41,42 @@ export function LabelsFieldInForm({ onEditClick }: LabelsFieldInFormProps) {
<Stack direction="column" gap={2}>
<Stack direction="column" gap={1}>
<Text element="h5">
<Trans i18nKey="alerting.labels-field-in-form.labels">Labels</Trans>
<Stack direction="row" alignItems="center" gap={0.5}>
{labelVariant === 'small' ? (
<Text variant="bodySmall">
<Trans i18nKey="alerting.labels-field-in-form.labels">Labels</Trans>
</Text>
) : (
<Text element="h5">
<Trans i18nKey="alerting.labels-field-in-form.labels">Labels</Trans>
</Text>
)}
<Text variant="bodySmall" color="secondary">
{t('alerting.common.optional', '(optional)')}
</Text>
{showHelpTooltip && (
<Tooltip
content={
<div>
<div style={{ marginBottom: 8 }}>
{t(
'alerting.labels-field-in-form.tooltip-text',
'Labels are used to differentiate an alert from all other alerts.You can use them for searching, silencing, and routing notifications.'
)}
</div>
<div>
{t(
'alerting.labels-field-in-form.tooltip-text-2',
'The dropdown only displays labels that you have previously used for alerts. Select a label from the options below or type in a new one.'
)}
</div>
</div>
}
>
<Icon name="info-circle" size="sm" />
</Tooltip>
)}
</Stack>
</Text>
<Stack direction={'column'} gap={1}>
<Stack direction={'row'} gap={1}>
@@ -6,8 +6,9 @@ import { urlUtil } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { config, locationService, logInfo } from '@grafana/runtime';
import { VizPanel } from '@grafana/scenes';
import { Alert, Button, Drawer } from '@grafana/ui';
import { Alert, Button } from '@grafana/ui';
import { LogMessages } from 'app/features/alerting/unified/Analytics';
import { AlertRuleDrawerForm } from 'app/features/alerting/unified/components/AlertRuleDrawerForm';
import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
interface ScenesNewRuleFromPanelButtonProps {
@@ -67,20 +68,12 @@ export const ScenesNewRuleFromPanelButton = ({ panel, className }: ScenesNewRule
className={className}
data-testid="create-alert-rule-button-drawer"
onClick={() => {
// logInfo(LogMessages.alertRuleFromPanel);
setIsOpen(true);
}}
>
<Trans i18nKey="alerting.new-rule-from-panel-button.new-alert-rule">New alert rule</Trans>
</Button>
{isOpen && (
<Drawer
title={t('alerting.new-rule-from-panel-button.new-alert-rule', 'New alert rule')}
onClose={() => setIsOpen(false)}
>
<Trans i18nKey="alerting.new-rule-from-panel-button.content-coming-soon">Content coming soon...</Trans>
</Drawer>
)}
<AlertRuleDrawerForm isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/consistent-type-assertions */
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
@@ -39,9 +40,9 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
new PanelDataTransformationsTab({ panelRef }),
];
if (shouldShowAlertingTab(panel.state.pluginId)) {
tabs.push(new PanelDataAlertingTab({ panelRef }));
}
// if (shouldShowAlertingTab(panel.state.pluginId)) {
tabs.push(new PanelDataAlertingTab({ panelRef }));
// }
return new PanelDataPane({
panelRef,