mirror of https://github.com/grafana/grafana
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
parent
974103c6fa
commit
9c15662cf6
|
@ -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; |
||||
} |
Loading…
Reference in new issue