mirror of https://github.com/grafana/grafana
Alerting: Add smart type selection when creating a new alert rule (#71071)
* Add smart type selection when creating a new alert rule * Auto switch when switch button has not been clicked yet * remove unnecessay code after the last refacgtor * Refactor * Remove unneeded prop * Move SmartAlertTypeDetector to its own file * Fix tests * Refactor: new useSetExpressionAndDataSource hook * Fix expressions not been propagated when switching from one type to another * Change texts * Update tests * Update text in switch button * Update texts and tests * Refactor: move code to getCanSwitch new method * Move smart alert after queries and remove auto-switch * Remove expressions and restore them when switching between grafana and cloud type * Rename previous expression state * Fix tests * Add data source name for data source-managed alert selection * Update reducer when changing cloud data source * PR review suggestions * PR review suggestions 2nd part * PR review suggestions 3th part * Fix canSwitch * Update texts on smart alert --------- Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>pull/71564/head^2
parent
bb48417ba0
commit
718401d250
@ -1,91 +0,0 @@ |
||||
import { render } from '@testing-library/react'; |
||||
import React from 'react'; |
||||
import { FormProvider, useForm } from 'react-hook-form'; |
||||
import { Provider } from 'react-redux'; |
||||
import { Router } from 'react-router-dom'; |
||||
import { byText } from 'testing-library-selector'; |
||||
|
||||
import { locationService } from '@grafana/runtime'; |
||||
import { contextSrv } from 'app/core/services/context_srv'; |
||||
import { configureStore } from 'app/store/configureStore'; |
||||
import { AccessControlAction } from 'app/types'; |
||||
|
||||
import { AlertType } from './AlertType'; |
||||
|
||||
const ui = { |
||||
ruleTypePicker: { |
||||
grafanaManagedButton: byText('Grafana managed alert'), |
||||
mimirOrLokiButton: byText('Mimir or Loki alert'), |
||||
mimirOrLokiRecordingButton: byText('Mimir or Loki recording rule'), |
||||
}, |
||||
}; |
||||
|
||||
const FormProviderWrapper = ({ children }: React.PropsWithChildren<{}>) => { |
||||
const methods = useForm({}); |
||||
return <FormProvider {...methods}>{children}</FormProvider>; |
||||
}; |
||||
|
||||
function renderAlertTypeStep() { |
||||
const store = configureStore(); |
||||
|
||||
render( |
||||
<Provider store={store}> |
||||
<Router history={locationService.getHistory()}> |
||||
<AlertType editingExistingRule={false} /> |
||||
</Router> |
||||
</Provider>, |
||||
{ wrapper: FormProviderWrapper } |
||||
); |
||||
} |
||||
|
||||
describe('RuleTypePicker', () => { |
||||
describe('RBAC', () => { |
||||
it('Should display grafana and mimir alert when user has rule create and write permissions', async () => { |
||||
jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => { |
||||
return [AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite].includes( |
||||
action as AccessControlAction |
||||
); |
||||
}); |
||||
|
||||
renderAlertTypeStep(); |
||||
|
||||
expect(ui.ruleTypePicker.grafanaManagedButton.get()).toBeInTheDocument(); |
||||
expect(ui.ruleTypePicker.mimirOrLokiButton.get()).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('Should not display the recording rule button', async () => { |
||||
jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => { |
||||
return [AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite].includes( |
||||
action as AccessControlAction |
||||
); |
||||
}); |
||||
|
||||
renderAlertTypeStep(); |
||||
expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.query()).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('Should hide grafana button when user does not have rule create permission', () => { |
||||
jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => { |
||||
return [AccessControlAction.AlertingRuleExternalWrite].includes(action as AccessControlAction); |
||||
}); |
||||
|
||||
renderAlertTypeStep(); |
||||
|
||||
expect(ui.ruleTypePicker.grafanaManagedButton.query()).not.toBeInTheDocument(); |
||||
expect(ui.ruleTypePicker.mimirOrLokiButton.get()).toBeInTheDocument(); |
||||
expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.query()).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('Should hide mimir alert and mimir recording when user does not have rule external write permission', () => { |
||||
jest.spyOn(contextSrv, 'hasPermission').mockImplementation((action) => { |
||||
return [AccessControlAction.AlertingRuleCreate].includes(action as AccessControlAction); |
||||
}); |
||||
|
||||
renderAlertTypeStep(); |
||||
|
||||
expect(ui.ruleTypePicker.grafanaManagedButton.get()).toBeInTheDocument(); |
||||
expect(ui.ruleTypePicker.mimirOrLokiButton.query()).not.toBeInTheDocument(); |
||||
expect(ui.ruleTypePicker.mimirOrLokiRecordingButton.query()).not.toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,180 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React from 'react'; |
||||
import { useFormContext } from 'react-hook-form'; |
||||
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; |
||||
import { Stack } from '@grafana/experimental'; |
||||
import { DataSourceJsonData } from '@grafana/schema'; |
||||
import { Alert, useStyles2 } from '@grafana/ui'; |
||||
import { contextSrv } from 'app/core/core'; |
||||
import { ExpressionDatasourceUID } from 'app/features/expressions/types'; |
||||
import { AccessControlAction } from 'app/types'; |
||||
import { AlertQuery } from 'app/types/unified-alerting-dto'; |
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; |
||||
import { NeedHelpInfo } from '../NeedHelpInfo'; |
||||
|
||||
function getAvailableRuleTypes() { |
||||
const canCreateGrafanaRules = contextSrv.hasAccess( |
||||
AccessControlAction.AlertingRuleCreate, |
||||
contextSrv.hasEditPermissionInFolders |
||||
); |
||||
const canCreateCloudRules = contextSrv.hasAccess(AccessControlAction.AlertingRuleExternalWrite, contextSrv.isEditor); |
||||
const defaultRuleType = canCreateGrafanaRules ? RuleFormType.grafana : RuleFormType.cloudAlerting; |
||||
|
||||
const enabledRuleTypes: RuleFormType[] = []; |
||||
if (canCreateGrafanaRules) { |
||||
enabledRuleTypes.push(RuleFormType.grafana); |
||||
} |
||||
if (canCreateCloudRules) { |
||||
enabledRuleTypes.push(RuleFormType.cloudAlerting, RuleFormType.cloudRecording); |
||||
} |
||||
|
||||
return { enabledRuleTypes, defaultRuleType }; |
||||
} |
||||
|
||||
const onlyOneDSInQueries = (queries: AlertQuery[]) => { |
||||
return queries.filter((q) => q.datasourceUid !== ExpressionDatasourceUID).length === 1; |
||||
}; |
||||
const getCanSwitch = ({ |
||||
queries, |
||||
ruleFormType, |
||||
editingExistingRule, |
||||
rulesSourcesWithRuler, |
||||
}: { |
||||
rulesSourcesWithRuler: Array<DataSourceInstanceSettings<DataSourceJsonData>>; |
||||
queries: AlertQuery[]; |
||||
ruleFormType: RuleFormType | undefined; |
||||
editingExistingRule: boolean; |
||||
}) => { |
||||
// get available rule types
|
||||
const availableRuleTypes = getAvailableRuleTypes(); |
||||
|
||||
// check if we have only one query in queries and if it's a cloud datasource
|
||||
const onlyOneDS = onlyOneDSInQueries(queries); |
||||
const dataSourceIdFromQueries = queries[0]?.datasourceUid ?? ''; |
||||
const isRecordingRuleType = ruleFormType === RuleFormType.cloudRecording; |
||||
|
||||
//let's check if we switch to cloud type
|
||||
const canSwitchToCloudRule = |
||||
!editingExistingRule && |
||||
!isRecordingRuleType && |
||||
onlyOneDS && |
||||
rulesSourcesWithRuler.some((dsJsonData) => dsJsonData.uid === dataSourceIdFromQueries); |
||||
|
||||
const canSwitchToGrafanaRule = !editingExistingRule && !isRecordingRuleType; |
||||
// check for enabled types
|
||||
const grafanaTypeEnabled = availableRuleTypes.enabledRuleTypes.includes(RuleFormType.grafana); |
||||
const cloudTypeEnabled = availableRuleTypes.enabledRuleTypes.includes(RuleFormType.cloudAlerting); |
||||
|
||||
// can we switch to the other type? (cloud or grafana)
|
||||
const canSwitchFromCloudToGrafana = |
||||
ruleFormType === RuleFormType.cloudAlerting && grafanaTypeEnabled && canSwitchToGrafanaRule; |
||||
const canSwitchFromGrafanaToCloud = |
||||
ruleFormType === RuleFormType.grafana && canSwitchToCloudRule && cloudTypeEnabled && canSwitchToCloudRule; |
||||
|
||||
return canSwitchFromCloudToGrafana || canSwitchFromGrafanaToCloud; |
||||
}; |
||||
|
||||
export interface SmartAlertTypeDetectorProps { |
||||
editingExistingRule: boolean; |
||||
rulesSourcesWithRuler: Array<DataSourceInstanceSettings<DataSourceJsonData>>; |
||||
queries: AlertQuery[]; |
||||
onClickSwitch: () => void; |
||||
} |
||||
|
||||
const getContentText = (ruleFormType: RuleFormType, isEditing: boolean, dataSourceName: string, canSwitch: boolean) => { |
||||
if (isEditing) { |
||||
if (ruleFormType === RuleFormType.grafana) { |
||||
return { |
||||
contentText: `Grafana-managed alert rules allow you to create alerts that can act on data from any of our supported data sources, including having multiple data sources in the same rule. You can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported. `, |
||||
title: `This alert rule is managed by Grafana.`, |
||||
}; |
||||
} else { |
||||
return { |
||||
contentText: `Data source-managed alert rules can be used for Grafana Mimir or Grafana Loki data sources which have been configured to support rule creation. The use of expressions or multiple queries is not supported.`, |
||||
title: `This alert rule is managed by the data source ${dataSourceName}.`, |
||||
}; |
||||
} |
||||
} |
||||
if (canSwitch) { |
||||
if (ruleFormType === RuleFormType.cloudAlerting) { |
||||
return { |
||||
contentText: |
||||
'Data source-managed alert rules can be used for Grafana Mimir or Grafana Loki data sources which have been configured to support rule creation. The use of expressions or multiple queries is not supported.', |
||||
title: `This alert rule is managed by the data source ${dataSourceName}. If you want to use expressions or have multiple queries, switch to a Grafana-managed alert rule.`, |
||||
}; |
||||
} else { |
||||
return { |
||||
contentText: |
||||
'Grafana-managed alert rules allow you to create alerts that can act on data from any of our supported data sources, including having multiple data sources in the same rule. You can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported.', |
||||
title: `This alert rule will be managed by Grafana. The selected data source is configured to support rule creation. You can switch to data source-managed alert rule.`, |
||||
}; |
||||
} |
||||
} else { |
||||
// it can be only grafana rule
|
||||
return { |
||||
contentText: `Grafana-managed alert rules allow you to create alerts that can act on data from any of our supported data sources, including having multiple data sources in the same rule. You can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported.`, |
||||
title: `Based on the selected data sources this alert rule will be managed by Grafana.`, |
||||
}; |
||||
} |
||||
}; |
||||
|
||||
export function SmartAlertTypeDetector({ |
||||
editingExistingRule, |
||||
rulesSourcesWithRuler, |
||||
queries, |
||||
onClickSwitch, |
||||
}: SmartAlertTypeDetectorProps) { |
||||
const { getValues } = useFormContext<RuleFormValues>(); |
||||
|
||||
const [ruleFormType, dataSourceName] = getValues(['type', 'dataSourceName']); |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
const canSwitch = getCanSwitch({ queries, ruleFormType, editingExistingRule, rulesSourcesWithRuler }); |
||||
|
||||
const typeTitle = |
||||
ruleFormType === RuleFormType.cloudAlerting ? 'Data source-managed alert rule' : 'Grafana-managed alert rule'; |
||||
const switchToLabel = ruleFormType !== RuleFormType.cloudAlerting ? 'data source-managed' : 'Grafana-managed'; |
||||
|
||||
const content = ruleFormType |
||||
? getContentText(ruleFormType, editingExistingRule, dataSourceName ?? '', canSwitch) |
||||
: undefined; |
||||
|
||||
return ( |
||||
<div className={styles.alert}> |
||||
<Alert |
||||
severity="info" |
||||
title={typeTitle} |
||||
onRemove={canSwitch ? onClickSwitch : undefined} |
||||
buttonContent={`Switch to ${switchToLabel} alert rule`} |
||||
> |
||||
<Stack gap={0.5} direction="row" alignItems={'baseline'}> |
||||
<div className={styles.alertText}>{content?.title}</div> |
||||
<div className={styles.needInfo}> |
||||
<NeedHelpInfo |
||||
contentText={content?.contentText ?? ''} |
||||
externalLink={`https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/alert-rule-types/`} |
||||
linkText={`Read about alert rule types`} |
||||
title=" Alert rule types" |
||||
/> |
||||
</div> |
||||
</Stack> |
||||
</Alert> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({ |
||||
alertText: css` |
||||
max-width: fit-content; |
||||
flex: 1; |
||||
`,
|
||||
alert: css` |
||||
margin-top: ${theme.spacing(2)}; |
||||
`,
|
||||
needInfo: css` |
||||
flex: 1; |
||||
max-width: fit-content; |
||||
`,
|
||||
}); |
Loading…
Reference in new issue