Alerting: Add feedback buttons for the new AI helpers (#108207)
* Add alerting ai buttons for cloud * add tracking for ai buttons usage * Empty commit to trigger GitHub Actions * wip * add FrontendOnly in ff * update analytics folder * prettier * revert ff being frontend only * add feedback button in templates * review comments * address some review comments * refactor * revert change and remove comment * prettier * update betterer * remove unused property * prettier * update AIFeedbackComponent * add feedback buttons for alert rule generation with ai * remove tracking from oss * fix analytics imports * udpdate changes in go.mod go.sum * prettier * update translations * update workspace * fix betterer * address pr comments * move most of the ai feedback buttons code to enterprise * prettier * fix * address pr feedback
This commit is contained in:
@@ -748,6 +748,11 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingAIGenAlertRules?: boolean;
|
||||
/**
|
||||
* Enable AI-generated feedback from the Grafana UI.
|
||||
* @default false
|
||||
*/
|
||||
alertingAIFeedback?: boolean;
|
||||
/**
|
||||
* Enable AI-improve alert rules labels and annotations.
|
||||
* @default false
|
||||
*/
|
||||
|
||||
@@ -1278,6 +1278,15 @@ var (
|
||||
HideFromDocs: true,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "alertingAIFeedback",
|
||||
Description: "Enable AI-generated feedback from the Grafana UI.",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
HideFromAdminPage: true,
|
||||
HideFromDocs: true,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "alertingAIImproveAlertRules",
|
||||
Description: "Enable AI-improve alert rules labels and annotations.",
|
||||
|
||||
@@ -167,6 +167,7 @@ crashDetection,experimental,@grafana/observability-traces-and-profiling,false,fa
|
||||
alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true
|
||||
azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false
|
||||
alertingAIGenAlertRules,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingAIFeedback,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingAIImproveAlertRules,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingAIGenTemplates,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,false,false
|
||||
|
||||
|
@@ -679,6 +679,10 @@ const (
|
||||
// Enable AI-generated alert rules.
|
||||
FlagAlertingAIGenAlertRules = "alertingAIGenAlertRules"
|
||||
|
||||
// FlagAlertingAIFeedback
|
||||
// Enable AI-generated feedback from the Grafana UI.
|
||||
FlagAlertingAIFeedback = "alertingAIFeedback"
|
||||
|
||||
// FlagAlertingAIImproveAlertRules
|
||||
// Enable AI-improve alert rules labels and annotations.
|
||||
FlagAlertingAIImproveAlertRules = "alertingAIImproveAlertRules"
|
||||
|
||||
@@ -89,6 +89,21 @@
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingAIFeedback",
|
||||
"resourceVersion": "1752820131286",
|
||||
"creationTimestamp": "2025-07-18T06:28:51Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable AI-generated feedback from the Grafana UI.",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"hideFromAdminPage": true,
|
||||
"hideFromDocs": true,
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingAIGenAlertRules",
|
||||
|
||||
@@ -107,6 +107,9 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
const [payload, setPayload] = useState(defaultPayloadString);
|
||||
const [payloadFormatError, setPayloadFormatError] = useState<string | null>(null);
|
||||
|
||||
// AI feedback state
|
||||
const [aiGeneratedTemplate, setAiGeneratedTemplate] = useState(false);
|
||||
|
||||
const { isProvisioned } = useNotificationTemplateMetadata(originalTemplate);
|
||||
const originalTemplatePrefill: TemplateFormValues | undefined = originalTemplate
|
||||
? { title: originalTemplate.title, content: originalTemplate.content }
|
||||
@@ -167,6 +170,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
|
||||
const handleTemplateGenerated = (template: string) => {
|
||||
setValue('content', template);
|
||||
setAiGeneratedTemplate(true);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -352,6 +356,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
||||
setPayloadFormatError={setPayloadFormatError}
|
||||
payloadFormatError={payloadFormatError}
|
||||
className={cx(styles.templatePreview, styles.minEditorSize)}
|
||||
aiGeneratedTemplate={aiGeneratedTemplate}
|
||||
setAiGeneratedTemplate={setAiGeneratedTemplate}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Box, Button, CodeEditor, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi';
|
||||
import { AIFeedbackButtonComponent } from '../../enterprise-components/AI/addAIFeedbackButton';
|
||||
import { stringifyErrorLike } from '../../utils/misc';
|
||||
import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader';
|
||||
|
||||
@@ -20,6 +21,8 @@ export function TemplatePreview({
|
||||
payloadFormatError,
|
||||
setPayloadFormatError,
|
||||
className,
|
||||
aiGeneratedTemplate,
|
||||
setAiGeneratedTemplate,
|
||||
}: {
|
||||
payload: string;
|
||||
templateName: string;
|
||||
@@ -27,6 +30,8 @@ export function TemplatePreview({
|
||||
payloadFormatError: string | null;
|
||||
setPayloadFormatError: (value: React.SetStateAction<string | null>) => void;
|
||||
className?: string;
|
||||
aiGeneratedTemplate?: boolean;
|
||||
setAiGeneratedTemplate?: (aiGeneratedTemplate: boolean) => void;
|
||||
}) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
@@ -48,7 +53,10 @@ export function TemplatePreview({
|
||||
disabled={isLoading}
|
||||
icon="sync"
|
||||
aria-label={t('alerting.template-preview.aria-label-refresh-preview', 'Refresh preview')}
|
||||
onClick={onPreview}
|
||||
onClick={() => {
|
||||
onPreview();
|
||||
setAiGeneratedTemplate?.(false);
|
||||
}}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
>
|
||||
@@ -56,6 +64,9 @@ export function TemplatePreview({
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className={styles.viewer.feedbackContainer}>
|
||||
<AIFeedbackButtonComponent origin="template" shouldShowFeedbackButton={Boolean(aiGeneratedTemplate)} />
|
||||
</div>
|
||||
<Box flex={1}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => <div className={styles.viewerContainer({ height })}>{previewToRender}</div>}
|
||||
@@ -163,6 +174,22 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
errorText: css({
|
||||
color: theme.colors.error.text,
|
||||
}),
|
||||
feedbackContainer: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderTop: `1px solid ${theme.colors.border.medium}`,
|
||||
backgroundColor: theme.colors.background.secondary,
|
||||
minHeight: 'auto',
|
||||
}),
|
||||
emptyState: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -174,10 +201,12 @@ export function getPreviewResults(
|
||||
// ERRORS IN JSON OR IN REQUEST (endpoint not available, for example)
|
||||
const previewErrorRequest = previewError ? stringifyErrorLike(previewError) : undefined;
|
||||
const errorToRender = payloadFormatError || previewErrorRequest;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
//PREVIEW : RESULTS AND ERRORS
|
||||
const previewResponseResults = data?.results ?? [];
|
||||
const previewResponseErrors = data?.errors;
|
||||
const hasContent = previewResponseResults.length > 0 || previewResponseErrors || errorToRender;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -187,7 +216,12 @@ export function getPreviewResults(
|
||||
</Alert>
|
||||
)}
|
||||
{previewResponseErrors && <PreviewErrorViewer errors={previewResponseErrors} />}
|
||||
{previewResponseResults && <PreviewResultViewer previews={previewResponseResults} />}
|
||||
{previewResponseResults.length > 0 && <PreviewResultViewer previews={previewResponseResults} />}
|
||||
{!hasContent && (
|
||||
<div className={styles.viewer.emptyState}>
|
||||
<Trans i18nKey="alerting.template-preview.empty-state">Add template content to see preview</Trans>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+5
@@ -40,6 +40,7 @@ import {
|
||||
RulerGroupUpdatedResponse,
|
||||
isGrafanaGroupUpdatedResponse,
|
||||
} from '../../../api/alertRuleModel';
|
||||
import { AIFeedbackButtonComponent } from '../../../enterprise-components/AI/addAIFeedbackButton';
|
||||
import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup';
|
||||
import {
|
||||
defaultFormValuesForRuleType,
|
||||
@@ -244,6 +245,10 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) =>
|
||||
<Stack direction="column" gap={3}>
|
||||
{/* Step 1 */}
|
||||
<AlertRuleNameAndMetric />
|
||||
|
||||
{/* AI Feedback: automatically shown for rules generated by AI */}
|
||||
<AIFeedbackButtonComponent origin="alert-rule" shouldShowFeedbackButton={true} useRouteDetection={true} />
|
||||
|
||||
{/* Step 2 */}
|
||||
<QueryAndExpressionsStep editingExistingRule={!!existing} onDataChange={checkAlertCondition} mode="edit" />
|
||||
{/* Step 3-4-5 */}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ComponentType, createElement } from 'react';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { withErrorBoundary } from '@grafana/ui';
|
||||
import { AIFeedbackOrigin } from 'app/extensions/alerting/AI/feedback/AIFeedbackComponent';
|
||||
|
||||
import { logError } from '../../Analytics';
|
||||
|
||||
export interface GenAIFeedbackButtonProps {
|
||||
origin: AIFeedbackOrigin;
|
||||
shouldShowFeedbackButton?: boolean;
|
||||
// If true, the component will use the route detection to determine if the feedback should be shown
|
||||
// this is necessary for example in alerting rule form, where the feedback button is shown in the same page as the rule form
|
||||
useRouteDetection?: boolean;
|
||||
}
|
||||
|
||||
let InternalAIFeedbackButtonComponent: ComponentType<GenAIFeedbackButtonProps> | null = null;
|
||||
|
||||
export const AIFeedbackButtonComponent: ComponentType<GenAIFeedbackButtonProps> = (props) => {
|
||||
if (!InternalAIFeedbackButtonComponent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Wrap the component with error boundary
|
||||
const WrappedComponent = withErrorBoundary(InternalAIFeedbackButtonComponent, {
|
||||
title: t('alerting.ai.error-boundary.feedback-button', 'AI Feedback Button failed to load'),
|
||||
style: 'alertbox',
|
||||
errorLogger: logError,
|
||||
});
|
||||
|
||||
// Provide default value for shouldShowFeedbackButton
|
||||
const propsWithDefaults = {
|
||||
shouldShowFeedbackButton: true,
|
||||
useRouteDetection: false,
|
||||
...props,
|
||||
};
|
||||
|
||||
return createElement(WrappedComponent, propsWithDefaults);
|
||||
};
|
||||
|
||||
export function addAIFeedbackButton(component: ComponentType<GenAIFeedbackButtonProps> | null) {
|
||||
InternalAIFeedbackButtonComponent = component;
|
||||
}
|
||||
@@ -383,6 +383,7 @@
|
||||
"ai": {
|
||||
"error-boundary": {
|
||||
"alert-rule-button": "AI Alert Rule Button failed to load",
|
||||
"feedback-button": "AI Feedback Button failed to load",
|
||||
"improve-annotations-button": "AI Improve Annotations Button failed to load",
|
||||
"improve-labels-button": "AI Improve Labels Button failed to load",
|
||||
"template-button": "AI Template Button failed to load",
|
||||
@@ -2803,6 +2804,7 @@
|
||||
},
|
||||
"template-preview": {
|
||||
"aria-label-refresh-preview": "Refresh preview",
|
||||
"empty-state": "Add template content to see preview",
|
||||
"label-preview": "Preview",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user