Alerting: Add AI buttons in some alerting workflows (#107754)

* Add alerting ai buttons for cloud

* add tracking for ai buttons usage

* Empty commit to trigger GitHub Actions

* add FrontendOnly in ff

* update analytics folder

* prettier

* revert ff being frontend only

* review comments

* address some review comments

* refactor

* revert change and remove comment

* prettier

* update betterer

* remove unused property

* prettier

* address review comments

* prettier

* update test

* fix linter errors

* add translations

* prettier

* update workspace

---------

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
pull/108109/head
Sonia Aguilar 4 days ago committed by GitHub
parent 974103c6fa
commit 9c15662cf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .betterer.results
  2. 2
      go.mod
  3. 4
      go.sum
  4. 20
      packages/grafana-data/src/types/featureToggles.gen.ts
  5. 36
      pkg/services/featuremgmt/registry.go
  6. 4
      pkg/services/featuremgmt/toggles_gen.csv
  7. 16
      pkg/services/featuremgmt/toggles_gen.go
  8. 89
      pkg/services/featuremgmt/toggles_gen.json
  9. 12
      public/app/features/alerting/unified/components/receivers/TemplateForm.tsx
  10. 3
      public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx
  11. 29
      public/app/features/alerting/unified/components/rule-editor/labels/LabelsFieldInForm.tsx
  12. 69
      public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
  13. 47
      public/app/features/alerting/unified/enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton.test.tsx
  14. 30
      public/app/features/alerting/unified/enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton.ts
  15. 51
      public/app/features/alerting/unified/enterprise-components/AI/AIGenImproveAnnotationsButton/addAIImproveAnnotationsButton.test.tsx
  16. 30
      public/app/features/alerting/unified/enterprise-components/AI/AIGenImproveAnnotationsButton/addAIImproveAnnotationsButton.ts
  17. 51
      public/app/features/alerting/unified/enterprise-components/AI/AIGenImproveLabelsButton/addAIImproveLabelsButton.test.tsx
  18. 30
      public/app/features/alerting/unified/enterprise-components/AI/AIGenImproveLabelsButton/addAIImproveLabelsButton.ts
  19. 52
      public/app/features/alerting/unified/enterprise-components/AI/AIGenTemplateButton/addAITemplateButton.test.tsx
  20. 33
      public/app/features/alerting/unified/enterprise-components/AI/AIGenTemplateButton/addAITemplateButton.ts
  21. 61
      public/app/features/alerting/unified/enterprise-components/AI/AIGenTriageButton/addAITriageButton.test.tsx
  22. 34
      public/app/features/alerting/unified/enterprise-components/AI/AIGenTriageButton/addAITriageButton.ts
  23. 18
      public/app/features/alerting/unified/rule-list/RuleList.v1.tsx
  24. 2
      public/app/features/alerting/unified/rule-list/RuleList.v2.tsx
  25. 2
      public/app/features/dashboard/components/GenAI/hooks.ts
  26. 9
      public/locales/en-US/grafana.json

@ -1280,9 +1280,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/rule-editor/AnnotationKeyInput.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"]
],
"public/app/features/alerting/unified/components/rule-editor/CloudEvaluationBehavior.tsx:5381": [
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "0"],
[0, 0, 0, "Add noMargin prop to Field components to remove built-in margins. Use layout components like Stack or Grid with the gap prop instead for consistent spacing.", "1"]

@ -230,7 +230,7 @@ require (
require (
github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b // @grafana/plugins-platform-backend
github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b // @grafana/alerting-backend
github.com/grafana/grafana/apps/dashboard v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-app-platform-squad @grafana/dashboards-squad
github.com/grafana/grafana/apps/dashboard v0.0.0-20250716152245-2202c99d7030 // @grafana/grafana-app-platform-squad @grafana/dashboards-squad
github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b // @grafana/grafana-search-and-storage
github.com/grafana/grafana/apps/iam v0.0.0-20250627191313-2f1a6ae1712b // @grafana/identity-access-team
github.com/grafana/grafana/apps/investigations v0.0.0-20250627191313-2f1a6ae1712b // @fcjack @matryer

@ -1611,8 +1611,8 @@ github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b h1:8o
github.com/grafana/grafana/apps/advisor v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:q+h3HbmqU/PposW6lq8cMle1v8vuyX1LCMrGzbabHxc=
github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b h1:jr+C3epmjhd5Yyob4P1Z/dPaW4LRTkU5UJLXsI4eaeM=
github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:WpI7TCck4P2wKTO2WJLBRcfOWvUGvTdxYu3QqS3z7jM=
github.com/grafana/grafana/apps/dashboard v0.0.0-20250627191313-2f1a6ae1712b h1:FJHoqhrVR/PMS9xv7yYGVi73iTB1VXyV/oIXCUssvMQ=
github.com/grafana/grafana/apps/dashboard v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:eR8wca74ADgxBrvX0uNpdB1qnPaGx/KhCm4Xj8oqHfQ=
github.com/grafana/grafana/apps/dashboard v0.0.0-20250716152245-2202c99d7030 h1:ravSwl2XJacO/o2u3IVaMacd0NpL1Msf9ENLcLbLwO0=
github.com/grafana/grafana/apps/dashboard v0.0.0-20250716152245-2202c99d7030/go.mod h1:1XWiRSVuDQiayapHhQiDc4S4e9GzEZgg/3GeNCuDgn4=
github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b h1:31MwoIKKT9Ay0ZjbT4lkfcPijiWogUWzXs2EjrCgodI=
github.com/grafana/grafana/apps/folder v0.0.0-20250627191313-2f1a6ae1712b/go.mod h1:dLtYBp1pza5HYalezNvzlP8JDeKrZ5BKTonDgEOE0NY=
github.com/grafana/grafana/apps/iam v0.0.0-20250627191313-2f1a6ae1712b h1:NV8v9xdM/pzjjy+1cLqUseia3bYcvQGh88vZdMW/jA0=

@ -749,6 +749,26 @@ export interface FeatureToggles {
*/
azureMonitorEnableUserAuth?: boolean;
/**
* Enable AI-generated alert rules.
* @default false
*/
alertingAIGenAlertRules?: boolean;
/**
* Enable AI-improve alert rules labels and annotations.
* @default false
*/
alertingAIImproveAlertRules?: boolean;
/**
* Enable AI-generated alerting templates.
* @default false
*/
alertingAIGenTemplates?: boolean;
/**
* Enable AI-analyze central state history.
* @default false
*/
alertingAIAnalyzeCentralStateHistory?: boolean;
/**
* Enables simplified step mode in the notifications section
* @default true
*/

@ -1277,6 +1277,42 @@ var (
Owner: grafanaPartnerPluginsSquad,
Expression: "true", // Enabled by default for now
},
{
Name: "alertingAIGenAlertRules",
Description: "Enable AI-generated alert rules.",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
HideFromAdminPage: true,
HideFromDocs: true,
Expression: "false",
},
{
Name: "alertingAIImproveAlertRules",
Description: "Enable AI-improve alert rules labels and annotations.",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
HideFromAdminPage: true,
HideFromDocs: true,
Expression: "false",
},
{
Name: "alertingAIGenTemplates",
Description: "Enable AI-generated alerting templates.",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
HideFromAdminPage: true,
HideFromDocs: true,
Expression: "false",
},
{
Name: "alertingAIAnalyzeCentralStateHistory",
Description: "Enable AI-analyze central state history.",
Stage: FeatureStageExperimental,
Owner: grafanaAlertingSquad,
HideFromAdminPage: true,
HideFromDocs: true,
Expression: "false",
},
{
Name: "alertingNotificationsStepMode",
Description: "Enables simplified step mode in the notifications section",

@ -167,6 +167,10 @@ enableSCIM,preview,@grafana/identity-access-team,false,false,false
crashDetection,experimental,@grafana/observability-traces-and-profiling,false,false,true
alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true
azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false
alertingAIGenAlertRules,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
alertingNotificationsStepMode,GA,@grafana/alerting-squad,false,false,true
feedbackButton,experimental,@grafana/grafana-operator-experience-squad,false,false,false
unifiedStorageSearchUI,experimental,@grafana/search-and-storage,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
167 crashDetection experimental @grafana/observability-traces-and-profiling false false true
168 alertingUIOptimizeReducer GA @grafana/alerting-squad false false true
169 azureMonitorEnableUserAuth GA @grafana/partner-datasources false false false
170 alertingAIGenAlertRules experimental @grafana/alerting-squad false false false
171 alertingAIImproveAlertRules experimental @grafana/alerting-squad false false false
172 alertingAIGenTemplates experimental @grafana/alerting-squad false false false
173 alertingAIAnalyzeCentralStateHistory experimental @grafana/alerting-squad false false false
174 alertingNotificationsStepMode GA @grafana/alerting-squad false false true
175 feedbackButton experimental @grafana/grafana-operator-experience-squad false false false
176 unifiedStorageSearchUI experimental @grafana/search-and-storage false false false

@ -679,6 +679,22 @@ const (
// Enables user auth for Azure Monitor datasource only
FlagAzureMonitorEnableUserAuth = "azureMonitorEnableUserAuth"
// FlagAlertingAIGenAlertRules
// Enable AI-generated alert rules.
FlagAlertingAIGenAlertRules = "alertingAIGenAlertRules"
// FlagAlertingAIImproveAlertRules
// Enable AI-improve alert rules labels and annotations.
FlagAlertingAIImproveAlertRules = "alertingAIImproveAlertRules"
// FlagAlertingAIGenTemplates
// Enable AI-generated alerting templates.
FlagAlertingAIGenTemplates = "alertingAIGenTemplates"
// FlagAlertingAIAnalyzeCentralStateHistory
// Enable AI-analyze central state history.
FlagAlertingAIAnalyzeCentralStateHistory = "alertingAIAnalyzeCentralStateHistory"
// FlagAlertingNotificationsStepMode
// Enables simplified step mode in the notifications section
FlagAlertingNotificationsStepMode = "alertingNotificationsStepMode"

@ -71,6 +71,78 @@
"expression": "false"
}
},
{
"metadata": {
"name": "alertingAIAnalyzeCentralStateHistory",
"resourceVersion": "1752215431801",
"creationTimestamp": "2025-07-07T14:40:14Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC"
}
},
"spec": {
"description": "Enable AI-analyze central state history.",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "alertingAIGenAlertRules",
"resourceVersion": "1752215431801",
"creationTimestamp": "2025-07-07T14:40:14Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC"
}
},
"spec": {
"description": "Enable AI-generated alert rules.",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "alertingAIGenTemplates",
"resourceVersion": "1752215431801",
"creationTimestamp": "2025-07-07T14:40:14Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC"
}
},
"spec": {
"description": "Enable AI-generated alerting templates.",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "alertingAIImproveAlertRules",
"resourceVersion": "1752215431801",
"creationTimestamp": "2025-07-07T14:40:14Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-07-11 06:30:31.801024 +0000 UTC"
}
},
"spec": {
"description": "Enable AI-improve alert rules labels and annotations.",
"stage": "experimental",
"codeowner": "@grafana/alerting-squad",
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "alertingBacktesting",
@ -963,6 +1035,23 @@
"expression": "false"
}
},
{
"metadata": {
"name": "enableAppChromeExtensions",
"resourceVersion": "1751899214406",
"creationTimestamp": "2025-07-07T14:40:14Z",
"deletionTimestamp": "2025-07-07T15:15:21Z"
},
"spec": {
"description": "Set this to true to enable all app chrome extensions registered by plugins.",
"stage": "experimental",
"codeowner": "@grafana/plugins-platform-backend",
"frontend": true,
"hideFromAdminPage": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "enableDatagridEditing",

@ -29,6 +29,7 @@ import { useAppNotification } from 'app/core/copy/appNotification';
import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints';
import { TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types';
import { AITemplateButtonComponent } from '../../enterprise-components/AI/AIGenTemplateButton/addAITemplateButton';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { makeAMLink, stringifyErrorLike } from '../../utils/misc';
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
@ -164,6 +165,10 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
setValue('content', newValue);
};
const handleTemplateGenerated = (template: string) => {
setValue('content', template);
};
return (
<>
<FormProvider {...formApi}>
@ -275,6 +280,13 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
</Button>
</Dropdown>
)}
{/* GenAI button – only available for Grafana Alertmanager and enterprise */}
{isGrafanaAlertManager && (
<AITemplateButtonComponent
onTemplateGenerated={handleTemplateGenerated}
disabled={isProvisioned}
/>
)}
<Button
icon="question-circle"
size="sm"

@ -9,6 +9,7 @@ import { Trans, t } from '@grafana/i18n';
import { Button, Field, Input, Stack, Text, TextArea, useStyles2 } from '@grafana/ui';
import { DashboardModel } from '../../../../dashboard/state/DashboardModel';
import { AIImproveAnnotationsButtonComponent } from '../../enterprise-components/AI/AIGenImproveAnnotationsButton/addAIImproveAnnotationsButton';
import { RuleFormValues } from '../../types/rule-form';
import { Annotation, annotationLabels } from '../../utils/constants';
import { isGrafanaManagedRuleByType } from '../../utils/rules';
@ -138,6 +139,7 @@ const AnnotationsStep = () => {
fullWidth
>
<Stack direction="column" gap={1}>
{isGrafanaManagedRuleByType(type) && <AIImproveAnnotationsButtonComponent />}
{fields.map((annotationField, index: number) => {
const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url');
const ValueInputComponent = isUrl ? Input : TextArea;
@ -173,6 +175,7 @@ const AnnotationsStep = () => {
className={cx(styles.flexRowItemMargin, styles.field)}
invalid={!!errors.annotations?.[index]?.value?.message}
error={errors.annotations?.[index]?.value?.message}
noMargin
>
<ValueInputComponent
data-testid={`annotation-value-${index}`}

@ -3,8 +3,9 @@ import { useFormContext } from 'react-hook-form';
import { Trans, t } from '@grafana/i18n';
import { Button, Stack, Text } from '@grafana/ui';
import { AIImproveLabelsButtonComponent } from '../../../enterprise-components/AI/AIGenImproveLabelsButton/addAIImproveLabelsButton';
import { RuleFormValues } from '../../../types/rule-form';
import { isRecordingRuleByType } from '../../../utils/rules';
import { isGrafanaManagedRuleByType, isRecordingRuleByType } from '../../../utils/rules';
import { NeedHelpInfo } from '../NeedHelpInfo';
import { LabelsInRule } from './LabelsField';
@ -19,6 +20,7 @@ export function LabelsFieldInForm({ onEditClick }: LabelsFieldInFormProps) {
const type = watch('type');
const isRecordingRule = type ? isRecordingRuleByType(type) : false;
const isGrafanaManaged = type ? isGrafanaManagedRuleByType(type) : false;
const text = isRecordingRule
? t('alerting.alertform.labels.recording', 'Add labels to your rule.')
@ -35,17 +37,22 @@ export function LabelsFieldInForm({ onEditClick }: LabelsFieldInFormProps) {
<Text element="h5">
<Trans i18nKey="alerting.labels-field-in-form.labels">Labels</Trans>
</Text>
<Stack direction={'row'} gap={1}>
<Text variant="bodySmall" color="secondary">
{text}
</Text>
<NeedHelpInfo
externalLink={'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/annotation-label/'}
linkText={`Read about labels`}
contentText="The dropdown only displays labels that you have previously used for alerts.
<Stack direction={'column'} gap={1}>
<Stack direction={'row'} gap={1}>
<Text variant="bodySmall" color="secondary">
{text}
</Text>
<NeedHelpInfo
externalLink={
'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/annotation-label/'
}
linkText={`Read about labels`}
contentText="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."
title={t('alerting.labels-field-in-form.title-labels', 'Labels')}
/>
title={t('alerting.labels-field-in-form.title-labels', 'Labels')}
/>
</Stack>
{isGrafanaManaged && <AIImproveLabelsButtonComponent />}
</Stack>
</Stack>
<Stack direction="row" gap={1} alignItems="center">

@ -25,6 +25,7 @@ import {
import { trackUseCentralHistoryFilterByClicking, trackUseCentralHistoryMaxEventsReached } from '../../../Analytics';
import { stateHistoryApi } from '../../../api/stateHistoryApi';
import { AITriageButtonComponent } from '../../../enterprise-components/AI/AIGenTriageButton/addAITriageButton';
import { usePagination } from '../../../hooks/usePagination';
import { combineMatcherStrings } from '../../../utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
@ -93,7 +94,7 @@ export const HistoryEventsList = ({
}
return (
<>
<Stack direction="column" gap={0.5}>
{maximumEventsReached && (
<Alert
severity="warning"
@ -107,7 +108,7 @@ export const HistoryEventsList = ({
)}
<LoadingIndicator visible={isLoading} />
<HistoryLogEvents logRecords={historyRecords} addFilter={addFilter} timeRange={timeRange} />
</>
</Stack>
);
};
@ -124,9 +125,17 @@ interface HistoryLogEventsProps {
}
function HistoryLogEvents({ logRecords, addFilter, timeRange }: HistoryLogEventsProps) {
const { page, pageItems, numberOfPages, onPageChange } = usePagination(logRecords, 1, PAGE_SIZE);
const styles = useStyles2(getStyles);
return (
<Stack direction="column" gap={0}>
<ListHeader />
<div className={styles.headerContainer}>
<ListHeader />
<div className={styles.triageButtonContainer}>
<AITriageButtonComponent logRecords={logRecords} timeRange={timeRange} />
</div>
</div>
<ul>
{pageItems.map((record) => {
return (
@ -148,28 +157,26 @@ function HistoryLogEvents({ logRecords, addFilter, timeRange }: HistoryLogEvents
function ListHeader() {
const styles = useStyles2(getStyles);
return (
<div className={styles.headerWrapper}>
<div className={styles.mainHeader}>
<div className={styles.timeCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.timestamp">Timestamp</Trans>
</Text>
</div>
<div className={styles.transitionCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.state">State</Trans>
</Text>
</div>
<div className={styles.alertNameCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.alert-rule">Alert rule</Trans>
</Text>
</div>
<div className={styles.labelsCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.instance">Instance</Trans>
</Text>
</div>
<div className={styles.mainHeader}>
<div className={styles.timeCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.timestamp">Timestamp</Trans>
</Text>
</div>
<div className={styles.transitionCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.state">State</Trans>
</Text>
</div>
<div className={styles.alertNameCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.alert-rule">Alert rule</Trans>
</Text>
</div>
<div className={styles.labelsCol}>
<Text variant="body">
<Trans i18nKey="alerting.central-alert-history.details.header.instance">Instance</Trans>
</Text>
</div>
</div>
);
@ -482,9 +489,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
cursor: 'pointer',
},
}),
headerWrapper: css({
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
mainHeader: css({
display: 'flex',
flexDirection: 'row',
@ -494,6 +498,15 @@ export const getStyles = (theme: GrafanaTheme2) => {
padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`,
gap: theme.spacing(0.5),
}),
headerContainer: css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
triageButtonContainer: css({
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
}),
};
};

@ -0,0 +1,47 @@
import { render, screen } from '@testing-library/react';
import { ComponentType } from 'react';
import { AIAlertRuleButtonComponent, GenAIAlertRuleButtonProps, addAIAlertRuleButton } from './addAIAlertRuleButton';
// Component that throws an error for testing
const ThrowingComponent: ComponentType<GenAIAlertRuleButtonProps> = () => {
throw new Error('Test error from AI component');
};
// Component that renders normally
const WorkingComponent: ComponentType<GenAIAlertRuleButtonProps> = () => {
return <div>AI Alert Rule Button</div>;
};
describe('AIAlertRuleButtonComponent Error Boundary', () => {
beforeEach(() => {
addAIAlertRuleButton(null);
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render null when no component is registered', () => {
const { container } = render(<AIAlertRuleButtonComponent />);
expect(container).toBeEmptyDOMElement();
});
it('should render the registered component when it works correctly', () => {
addAIAlertRuleButton(WorkingComponent);
render(<AIAlertRuleButtonComponent />);
expect(screen.getByText('AI Alert Rule Button')).toBeInTheDocument();
});
it('should gracefully handle errors from AI components with error boundary', () => {
addAIAlertRuleButton(ThrowingComponent);
// Render the component, it should not crash the page
render(<AIAlertRuleButtonComponent />);
expect(screen.getByText('AI Alert Rule Button failed to load')).toBeInTheDocument();
// Check for error alert role instead of direct DOM access
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});

@ -0,0 +1,30 @@
import { ComponentType, createElement } from 'react';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../Analytics';
export interface GenAIAlertRuleButtonProps {}
// Internal variable to store the actual component
let InternalAIAlertRuleButtonComponent: ComponentType<GenAIAlertRuleButtonProps> | null = null;
export const AIAlertRuleButtonComponent: ComponentType<GenAIAlertRuleButtonProps> = (props) => {
if (!InternalAIAlertRuleButtonComponent) {
return null;
}
// Wrap the component with error boundary
const WrappedComponent = withErrorBoundary(InternalAIAlertRuleButtonComponent, {
title: t('alerting.ai.error-boundary.alert-rule-button', 'AI Alert Rule Button failed to load'),
style: 'alertbox',
errorLogger: logError,
});
return createElement(WrappedComponent, props);
};
export function addAIAlertRuleButton(component: ComponentType<GenAIAlertRuleButtonProps> | null) {
InternalAIAlertRuleButtonComponent = component;
}

@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react';
import { ComponentType } from 'react';
import {
AIImproveAnnotationsButtonComponent,
GenAIImproveAnnotationsButtonProps,
addAIImproveAnnotationsButton,
} from './addAIImproveAnnotationsButton';
// Component that throws an error for testing
const ThrowingComponent: ComponentType<GenAIImproveAnnotationsButtonProps> = () => {
throw new Error('Test error from AI component');
};
// Component that renders normally
const WorkingComponent: ComponentType<GenAIImproveAnnotationsButtonProps> = () => {
return <div>AI Improve Annotations Button</div>;
};
describe('AIImproveAnnotationsButtonComponent Error Boundary', () => {
beforeEach(() => {
addAIImproveAnnotationsButton(null);
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render null when no component is registered', () => {
const { container } = render(<AIImproveAnnotationsButtonComponent />);
expect(container).toBeEmptyDOMElement();
});
it('should render the registered component when it works correctly', () => {
addAIImproveAnnotationsButton(WorkingComponent);
render(<AIImproveAnnotationsButtonComponent />);
expect(screen.getByText('AI Improve Annotations Button')).toBeInTheDocument();
});
it('should gracefully handle errors from AI components with error boundary', () => {
addAIImproveAnnotationsButton(ThrowingComponent);
// Render the component, it should not crash the page
render(<AIImproveAnnotationsButtonComponent />);
expect(screen.getByText('AI Improve Annotations Button failed to load')).toBeInTheDocument();
// Check for error alert role instead of direct DOM access
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});

@ -0,0 +1,30 @@
import { ComponentType, createElement } from 'react';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../Analytics';
export interface GenAIImproveAnnotationsButtonProps {}
// Internal variable to store the actual component
let InternalAIImproveAnnotationsButtonComponent: ComponentType<GenAIImproveAnnotationsButtonProps> | null = null;
export const AIImproveAnnotationsButtonComponent: ComponentType<GenAIImproveAnnotationsButtonProps> = (props) => {
if (!InternalAIImproveAnnotationsButtonComponent) {
return null;
}
// Wrap the component with error boundary
const WrappedComponent = withErrorBoundary(InternalAIImproveAnnotationsButtonComponent, {
title: t('alerting.ai.error-boundary.improve-annotations-button', 'AI Improve Annotations Button failed to load'),
style: 'alertbox',
errorLogger: logError,
});
return createElement(WrappedComponent, props);
};
export function addAIImproveAnnotationsButton(component: ComponentType<GenAIImproveAnnotationsButtonProps> | null) {
InternalAIImproveAnnotationsButtonComponent = component;
}

@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react';
import { ComponentType } from 'react';
import {
AIImproveLabelsButtonComponent,
GenAIImproveLabelsButtonProps,
addAIImproveLabelsButton,
} from './addAIImproveLabelsButton';
// Component that throws an error for testing
const ThrowingComponent: ComponentType<GenAIImproveLabelsButtonProps> = () => {
throw new Error('Test error from AI component');
};
// Component that renders normally
const WorkingComponent: ComponentType<GenAIImproveLabelsButtonProps> = () => {
return <div>AI Improve Labels Button</div>;
};
describe('AIImproveLabelsButtonComponent Error Boundary', () => {
beforeEach(() => {
addAIImproveLabelsButton(null);
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render null when no component is registered', () => {
const { container } = render(<AIImproveLabelsButtonComponent />);
expect(container).toBeEmptyDOMElement();
});
it('should render the registered component when it works correctly', () => {
addAIImproveLabelsButton(WorkingComponent);
render(<AIImproveLabelsButtonComponent />);
expect(screen.getByText('AI Improve Labels Button')).toBeInTheDocument();
});
it('should gracefully handle errors from AI components with error boundary', () => {
addAIImproveLabelsButton(ThrowingComponent);
// Render the component, it should not crash the page
render(<AIImproveLabelsButtonComponent />);
expect(screen.getByText('AI Improve Labels Button failed to load')).toBeInTheDocument();
// Check for error alert role instead of direct DOM access
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});

@ -0,0 +1,30 @@
import { ComponentType, createElement } from 'react';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../Analytics';
export interface GenAIImproveLabelsButtonProps {}
// Internal variable to store the actual component
let InternalAIImproveLabelsButtonComponent: ComponentType<GenAIImproveLabelsButtonProps> | null = null;
export const AIImproveLabelsButtonComponent: ComponentType<GenAIImproveLabelsButtonProps> = (props) => {
if (!InternalAIImproveLabelsButtonComponent) {
return null;
}
// Wrap the component with error boundary
const WrappedComponent = withErrorBoundary(InternalAIImproveLabelsButtonComponent, {
title: t('alerting.ai.error-boundary.improve-labels-button', 'AI Improve Labels Button failed to load'),
style: 'alertbox',
errorLogger: logError,
});
return createElement(WrappedComponent, props);
};
export function addAIImproveLabelsButton(component: ComponentType<GenAIImproveLabelsButtonProps> | null) {
InternalAIImproveLabelsButtonComponent = component;
}

@ -0,0 +1,52 @@
import { render, screen } from '@testing-library/react';
import { ComponentType } from 'react';
import { AITemplateButtonComponent, GenAITemplateButtonProps, addAITemplateButton } from './addAITemplateButton';
// Component that throws an error for testing
const ThrowingComponent: ComponentType<GenAITemplateButtonProps> = () => {
throw new Error('Test error from AI component');
};
// Component that renders normally
const WorkingComponent: ComponentType<GenAITemplateButtonProps> = () => {
return <div>AI Template Button</div>;
};
const mockProps: GenAITemplateButtonProps = {
onTemplateGenerated: jest.fn(),
disabled: false,
};
describe('AITemplateButtonComponent Error Boundary', () => {
beforeEach(() => {
addAITemplateButton(null);
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render null when no component is registered', () => {
const { container } = render(<AITemplateButtonComponent {...mockProps} />);
expect(container).toBeEmptyDOMElement();
});
it('should render the registered component when it works correctly', () => {
addAITemplateButton(WorkingComponent);
render(<AITemplateButtonComponent {...mockProps} />);
expect(screen.getByText('AI Template Button')).toBeInTheDocument();
});
it('should gracefully handle errors from AI components with error boundary', () => {
addAITemplateButton(ThrowingComponent);
// Render the component, it should not crash the page
render(<AITemplateButtonComponent {...mockProps} />);
expect(screen.getByText('AI Template Button failed to load')).toBeInTheDocument();
// Check for error alert role instead of direct DOM access
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});

@ -0,0 +1,33 @@
import { ComponentType, createElement } from 'react';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../Analytics';
export interface GenAITemplateButtonProps {
onTemplateGenerated: (template: string) => void;
disabled?: boolean;
}
let InternalAITemplateButtonComponent: ComponentType<GenAITemplateButtonProps> | null = null;
// this is the component that is used by the consumer in the grafana repo
export const AITemplateButtonComponent: ComponentType<GenAITemplateButtonProps> = (props) => {
if (!InternalAITemplateButtonComponent) {
return null;
}
// Wrap the component with error boundary
const WrappedComponent = withErrorBoundary(InternalAITemplateButtonComponent, {
title: t('alerting.ai.error-boundary.template-button', 'AI Template Button failed to load'),
style: 'alertbox',
errorLogger: logError,
});
return createElement(WrappedComponent, props);
};
export function addAITemplateButton(component: ComponentType<GenAITemplateButtonProps> | null) {
InternalAITemplateButtonComponent = component;
}

@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/react';
import { ComponentType } from 'react';
import { dateTime } from '@grafana/data';
import { AITriageButtonComponent, GenAITriageButtonProps, addAITriageButton } from './addAITriageButton';
// Component that throws an error for testing
const ThrowingComponent: ComponentType<GenAITriageButtonProps> = () => {
throw new Error('Test error from AI component');
};
// Component that renders normally
const WorkingComponent: ComponentType<GenAITriageButtonProps> = () => {
return <div>AI Triage Button</div>;
};
const mockProps: GenAITriageButtonProps = {
logRecords: [],
timeRange: {
from: dateTime(1681300292392),
to: dateTime(1681300293392),
raw: {
from: 'now-1s',
to: 'now',
},
},
};
describe('AITriageButtonComponent Error Boundary', () => {
beforeEach(() => {
addAITriageButton(null);
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render null when no component is registered', () => {
const { container } = render(<AITriageButtonComponent {...mockProps} />);
expect(container).toBeEmptyDOMElement();
});
it('should render the registered component when it works correctly', () => {
addAITriageButton(WorkingComponent);
render(<AITriageButtonComponent {...mockProps} />);
expect(screen.getByText('AI Triage Button')).toBeInTheDocument();
});
it('should gracefully handle errors from AI components with error boundary', () => {
addAITriageButton(ThrowingComponent);
// Render the component, it should not crash the page
render(<AITriageButtonComponent {...mockProps} />);
expect(screen.getByText('AI Triage Button failed to load')).toBeInTheDocument();
// Check for error alert role instead of direct DOM access
expect(screen.getByRole('alert')).toBeInTheDocument();
});
});

@ -0,0 +1,34 @@
import { ComponentType, createElement } from 'react';
import { TimeRange } from '@grafana/data';
import { t } from '@grafana/i18n';
import { withErrorBoundary } from '@grafana/ui';
import { logError } from '../../../Analytics';
import { LogRecord } from '../../../components/rules/state-history/common';
export interface GenAITriageButtonProps {
logRecords: LogRecord[];
timeRange: TimeRange;
}
let InternalAITriageButtonComponent: ComponentType<GenAITriageButtonProps> | null = null;
export const AITriageButtonComponent: ComponentType<GenAITriageButtonProps> = (props) => {
if (!InternalAITriageButtonComponent) {
return null;
}
// Wrap the component with error boundary
const WrappedComponent = withErrorBoundary(InternalAITriageButtonComponent, {
title: t('alerting.ai.error-boundary.triage-button', 'AI Triage Button failed to load'),
style: 'alertbox',
errorLogger: logError,
});
return createElement(WrappedComponent, props);
};
export function addAITriageButton(component: ComponentType<GenAITriageButtonProps> | null) {
InternalAITriageButtonComponent = component;
}

@ -19,6 +19,7 @@ import { RuleListErrors } from '../components/rules/RuleListErrors';
import { RuleListGroupView } from '../components/rules/RuleListGroupView';
import { RuleListStateView } from '../components/rules/RuleListStateView';
import { RuleStats } from '../components/rules/RuleStats';
import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton';
import { shouldUsePrometheusRulesPrimary } from '../featureToggles';
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces';
@ -174,13 +175,16 @@ export function CreateAlertButton() {
if (canCreateGrafanaRules || canCreateCloudRules) {
return (
<LinkButton
href={urlUtil.renderUrl('alerting/new/alerting', { returnTo: location.pathname + location.search })}
icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
>
<Trans i18nKey="alerting.rule-list.new-alert-rule">New alert rule</Trans>
</LinkButton>
<Stack direction="row" gap={1}>
<LinkButton
href={urlUtil.renderUrl('alerting/new/alerting', { returnTo: location.pathname + location.search })}
icon="plus"
onClick={() => logInfo(LogMessages.alertRuleFromScratch)}
>
<Trans i18nKey="alerting.rule-list.new-alert-rule">New alert rule</Trans>
</LinkButton>
{canCreateGrafanaRules && AIAlertRuleButtonComponent && <AIAlertRuleButtonComponent />}
</Stack>
);
}
return null;

@ -7,6 +7,7 @@ import { Button, Dropdown, Icon, LinkButton, Menu, Stack } from '@grafana/ui';
import { AlertingPageWrapper } from '../components/AlertingPageWrapper';
import RulesFilter from '../components/rules/Filter/RulesFilter';
import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelector';
import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton';
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
import { useRulesFilter } from '../hooks/useFilteredRules';
import { isAdmin } from '../utils/misc';
@ -86,6 +87,7 @@ export function RuleListActions() {
<Trans i18nKey="alerting.rule-list.new-alert-rule">New alert rule</Trans>
</LinkButton>
)}
{canCreateGrafanaRules && <AIAlertRuleButtonComponent />}
<Dropdown overlay={moreActionsMenu}>
<Button variant="secondary">
<Trans i18nKey="alerting.rule-list.more">More</Trans> <Icon name="angle-down" />

@ -6,7 +6,7 @@ import { llm } from '@grafana/llm';
import { createMonitoringLogger } from '@grafana/runtime';
import { useAppNotification } from 'app/core/copy/appNotification';
import { isLLMPluginEnabled, DEFAULT_LLM_MODEL } from './utils';
import { DEFAULT_LLM_MODEL, isLLMPluginEnabled } from './utils';
// Declared instead of imported from utils to make this hook modular
// Ideally we will want to move the hook itself to a different scope later.

@ -380,6 +380,15 @@
"add-button": {
"add-more": "Add more"
},
"ai": {
"error-boundary": {
"alert-rule-button": "AI Alert Rule 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",
"triage-button": "AI Triage Button failed to load"
}
},
"alert": {
"alert-state": "Alert state",
"annotations": "Annotations",

Loading…
Cancel
Save