diff --git a/public/app/features/alerting/unified/components/AlertRuleDrawerForm.tsx b/public/app/features/alerting/unified/components/AlertRuleDrawerForm.tsx new file mode 100644 index 00000000000..f15a192ce6a --- /dev/null +++ b/public/app/features/alerting/unified/components/AlertRuleDrawerForm.tsx @@ -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({ defaultValues: getDefaultFormValues(RuleFormType.grafana) }); + + if (!isOpen) { + return null; + } + + return ( + + + + + + ); +} diff --git a/public/app/features/alerting/unified/components/RuleDefinitionSection.tsx b/public/app/features/alerting/unified/components/RuleDefinitionSection.tsx new file mode 100644 index 00000000000..6b27167763b --- /dev/null +++ b/public/app/features/alerting/unified/components/RuleDefinitionSection.tsx @@ -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(); + const [showLabelsEditor, setShowLabelsEditor] = useState(false); + + const isRecording = isRecordingRuleByType(type); + const isCloudRecordingRule = isCloudRecordingRuleByType(type); + const namePlaceholder = isRecording ? 'recording rule' : 'alert rule'; + + return ( +
+
+ + 1 + +
+ Rule Definition +
+
+
+ + Name} + error={errors?.name?.message} + invalid={!!errors.name?.message} + > + + + + {isGrafanaManagedRuleByType(type) && ( + <> + + setShowLabelsEditor(true)} labelVariant="small" /> + { + if (labelsToUpdate) { + setValue('labels', labelsToUpdate); + } + setShowLabelsEditor(false); + }} + dataSourceName={GRAFANA_RULES_SOURCE_NAME} + initialLabels={getValues('labels')} + /> + + )} + +
+
+ ); +} + +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)})` }), + }; +} diff --git a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx index 94a5ecfe849..89a012f0d98 100644 --- a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx +++ b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx @@ -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)} > New folder diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx index 44224012093..69826423207 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx @@ -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)} > New alert rule - setIsOpen(false)} - > - Content coming soon... - + setIsOpen(false)} /> ); } diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderSelectorV2.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderSelectorV2.tsx new file mode 100644 index 00000000000..4ee9be33bcc --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/FolderSelectorV2.tsx @@ -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(); + + const resetGroup = useCallback(() => { + setValue('group', ''); + }, [setValue]); + + const folder = watch('folder'); + + const handleFolderCreation = (folder: Folder) => { + resetGroup(); + setValue('folder', folder); + }; + + return ( + + { + + + Folder + + + + + + } + error={errors.folder?.message} + data-testid="folder-picker" + > + + ( +
+ { + if (uid && title) { + setValue('folder', { title, uid }); + } else { + setValue('folder', undefined); + } + + resetGroup(); + }} + /> +
+ )} + name="folder" + rules={{ + required: { + value: true, + message: t('alerting.folder-selector.message.select-a-folder', 'Select a folder'), + }, + }} + /> + +
+
+ } +
+ ); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx index d6217be0c59..3b5bd9589ee 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx @@ -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(); const labels = watch('labels'); @@ -35,7 +41,42 @@ export function LabelsFieldInForm({ onEditClick }: LabelsFieldInFormProps) { - Labels + + {labelVariant === 'small' ? ( + + Labels + + ) : ( + + Labels + + )} + + {t('alerting.common.optional', '(optional)')} + + {showHelpTooltip && ( + +
+ {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.' + )} +
+
+ {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.' + )} +
+ + } + > + +
+ )} +
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/NewAlertRuleButton.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/NewAlertRuleButton.tsx index 013bd1bcc65..f423619200c 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/NewAlertRuleButton.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/NewAlertRuleButton.tsx @@ -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); }} > New alert rule - {isOpen && ( - setIsOpen(false)} - > - Content coming soon... - - )} + setIsOpen(false)} /> ); } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index de5d15aca86..1b9011dea15 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -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 { 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,