2024-02-07 18:02:20 +01:00
import { css } from '@emotion/css' ;
2024-01-02 10:05:58 +01:00
import { isEmpty , truncate } from 'lodash' ;
2024-01-23 15:04:12 +01:00
import React , { useState } from 'react' ;
2023-07-06 13:16:47 +02:00
2024-01-23 15:04:12 +01:00
import { NavModelItem , UrlQueryValue } from '@grafana/data' ;
2024-02-07 18:02:20 +01:00
import { Alert , LinkButton , Stack , TabContent , Text , TextLink , useStyles2 } from '@grafana/ui' ;
2024-01-02 10:05:58 +01:00
import { PageInfoItem } from 'app/core/components/Page/types' ;
import { useQueryParams } from 'app/core/hooks/useQueryParams' ;
2024-03-28 15:22:09 +00:00
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule' ;
2024-02-07 18:02:20 +01:00
import { CombinedRule , RuleHealth , RuleIdentifier } from 'app/types/unified-alerting' ;
import { PromAlertingRuleState , PromRuleType } from 'app/types/unified-alerting-dto' ;
2023-07-06 13:16:47 +02:00
2024-03-14 15:18:01 +01:00
import { defaultPageNav } from '../../RuleViewer' ;
2024-04-30 10:34:52 +02:00
import { PluginOriginBadge } from '../../plugins/PluginOriginBadge' ;
2024-03-14 15:18:01 +01:00
import { Annotation } from '../../utils/constants' ;
import { makeDashboardLink , makePanelLink } from '../../utils/misc' ;
2024-03-25 18:10:16 +00:00
import {
2024-04-30 10:34:52 +02:00
RulePluginOrigin ,
getRulePluginOrigin ,
2024-03-25 18:10:16 +00:00
isAlertingRule ,
isFederatedRuleGroup ,
isGrafanaRulerRule ,
isGrafanaRulerRulePaused ,
isRecordingRule ,
} from '../../utils/rules' ;
2024-03-14 15:18:01 +01:00
import { createUrl } from '../../utils/url' ;
import { AlertLabels } from '../AlertLabels' ;
import { AlertingPageWrapper } from '../AlertingPageWrapper' ;
import { ProvisionedResource , ProvisioningAlert } from '../Provisioning' ;
import { WithReturnButton } from '../WithReturnButton' ;
import { decodeGrafanaNamespace } from '../expressions/util' ;
import { RedirectToCloneRule } from '../rules/CloneRule' ;
2023-07-06 13:16:47 +02:00
2024-01-23 15:04:12 +01:00
import { useAlertRulePageActions } from './Actions' ;
2024-01-02 10:05:58 +01:00
import { useDeleteModal } from './DeleteModal' ;
2024-03-14 15:18:01 +01:00
import { FederatedRuleWarning } from './FederatedRuleWarning' ;
2024-03-27 16:50:41 +01:00
import PausedBadge from './PausedBadge' ;
2024-01-23 15:04:12 +01:00
import { useAlertRule } from './RuleContext' ;
2024-02-07 18:02:20 +01:00
import { RecordingBadge , StateBadge } from './StateBadges' ;
2024-03-14 15:18:01 +01:00
import { Details } from './tabs/Details' ;
import { History } from './tabs/History' ;
import { InstancesList } from './tabs/Instances' ;
import { QueryResults } from './tabs/Query' ;
import { Routing } from './tabs/Routing' ;
2024-01-02 10:05:58 +01:00
enum ActiveTab {
Query = 'query' ,
Instances = 'instances' ,
History = 'history' ,
Routing = 'routing' ,
Details = 'details' ,
2023-07-06 13:16:47 +02:00
}
2024-01-23 15:04:12 +01:00
const RuleViewer = ( ) = > {
const { rule } = useAlertRule ( ) ;
2024-01-02 10:05:58 +01:00
const { pageNav , activeTab } = usePageNav ( rule ) ;
2023-08-08 20:36:38 +02:00
2024-01-23 15:04:12 +01:00
// this will be used to track if we are in the process of cloning a rule
// we want to be able to show a modal if the rule has been provisioned explain the limitations
// of duplicating provisioned alert rules
const [ duplicateRuleIdentifier , setDuplicateRuleIdentifier ] = useState < RuleIdentifier > ( ) ;
2023-07-06 13:16:47 +02:00
2024-01-23 15:04:12 +01:00
const [ deleteModal , showDeleteModal ] = useDeleteModal ( ) ;
const actions = useAlertRulePageActions ( {
handleDuplicateRule : setDuplicateRuleIdentifier ,
handleDelete : showDeleteModal ,
} ) ;
2024-01-02 10:05:58 +01:00
2024-02-07 18:02:20 +01:00
const { annotations , promRule } = rule ;
const hasError = isErrorHealth ( rule . promRule ? . health ) ;
2024-01-02 10:05:58 +01:00
const isAlertType = isAlertingRule ( promRule ) ;
2023-07-06 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
const isFederatedRule = isFederatedRuleGroup ( rule . group ) ;
const isProvisioned = isGrafanaRulerRule ( rule . rulerRule ) && Boolean ( rule . rulerRule . grafana_alert . provenance ) ;
2024-03-25 18:10:16 +00:00
const isPaused = isGrafanaRulerRule ( rule . rulerRule ) && isGrafanaRulerRulePaused ( rule . rulerRule ) ;
2024-01-02 10:05:58 +01:00
2024-03-27 16:50:41 +01:00
const showError = hasError && ! isPaused ;
2024-04-30 10:34:52 +02:00
const ruleOrigin = getRulePluginOrigin ( rule ) ;
2024-03-27 16:50:41 +01:00
2024-02-07 18:02:20 +01:00
const summary = annotations [ Annotation . summary ] ;
2024-03-27 16:50:41 +01:00
2024-01-02 10:05:58 +01:00
return (
< AlertingPageWrapper
pageNav = { pageNav }
navId = "alert-list"
isLoading = { false }
2024-02-07 18:02:20 +01:00
renderTitle = { ( title ) = > (
< Title
name = { title }
2024-03-27 16:50:41 +01:00
paused = { isPaused }
state = { isAlertType ? promRule.state : undefined }
2024-02-07 18:02:20 +01:00
health = { rule . promRule ? . health }
ruleType = { rule . promRule ? . type }
2024-04-30 10:34:52 +02:00
ruleOrigin = { ruleOrigin }
2024-02-07 18:02:20 +01:00
/ >
) }
2024-01-23 15:04:12 +01:00
actions = { actions }
2024-01-02 10:05:58 +01:00
info = { createMetadata ( rule ) }
2024-02-07 18:02:20 +01:00
subTitle = {
< Stack direction = "column" >
2024-03-28 15:22:09 +00:00
{ isPaused && < InfoPausedRule / > }
2024-02-07 18:02:20 +01:00
{ summary }
2023-07-06 13:16:47 +02:00
{ /* alerts and notifications and stuff */ }
2024-02-07 18:02:20 +01:00
{ isFederatedRule && < FederatedRuleWarning / > }
{ /* indicator for rules in a provisioned group */ }
{ isProvisioned && (
< ProvisioningAlert resource = { ProvisionedResource . AlertRule } bottomSpacing = { 0 } topSpacing = { 2 } / >
) }
{ /* error state */ }
2024-03-27 16:50:41 +01:00
{ showError && (
2024-02-07 18:02:20 +01:00
< Alert title = "Something went wrong when evaluating this alert rule" bottomSpacing = { 0 } topSpacing = { 2 } >
< pre style = { { marginBottom : 0 } } >
< code > { rule . promRule ? . lastError ? ? 'No error message' } < / code >
< / pre >
2023-07-06 13:16:47 +02:00
< / Alert >
) }
< / Stack >
2024-02-07 18:02:20 +01:00
}
>
< Stack direction = "column" gap = { 2 } >
{ /* tabs and tab content */ }
< TabContent >
{ activeTab === ActiveTab . Query && < QueryResults rule = { rule } / > }
{ activeTab === ActiveTab . Instances && < InstancesList rule = { rule } / > }
{ activeTab === ActiveTab . History && isGrafanaRulerRule ( rule . rulerRule ) && < History rule = { rule . rulerRule } / > }
{ activeTab === ActiveTab . Routing && < Routing / > }
{ activeTab === ActiveTab . Details && < Details rule = { rule } / > }
< / TabContent >
2024-01-02 10:05:58 +01:00
< / Stack >
{ deleteModal }
2024-01-23 15:04:12 +01:00
{ duplicateRuleIdentifier && (
< RedirectToCloneRule
redirectTo = { true }
identifier = { duplicateRuleIdentifier }
isProvisioned = { isProvisioned }
onDismiss = { ( ) = > setDuplicateRuleIdentifier ( undefined ) }
/ >
) }
2024-01-02 10:05:58 +01:00
< / AlertingPageWrapper >
) ;
} ;
const createMetadata = ( rule : CombinedRule ) : PageInfoItem [ ] = > {
const { labels , annotations , group } = rule ;
const metadata : PageInfoItem [ ] = [ ] ;
const runbookUrl = annotations [ Annotation . runbookURL ] ;
const dashboardUID = annotations [ Annotation . dashboardUID ] ;
const panelID = annotations [ Annotation . panelID ] ;
2024-02-07 18:02:20 +01:00
const hasDashboardAndPanel = dashboardUID && panelID ;
const hasDashboard = dashboardUID ;
2024-01-02 10:05:58 +01:00
const hasLabels = ! isEmpty ( labels ) ;
const interval = group . interval ;
if ( runbookUrl ) {
metadata . push ( {
label : 'Runbook' ,
value : (
< TextLink variant = "bodySmall" href = { runbookUrl } external >
{ /* TODO instead of truncating the string, we should use flex and text overflow properly to allow it to take up all of the horizontal space available */ }
{ truncate ( runbookUrl , { length : 42 } ) }
< / TextLink >
) ,
} ) ;
}
2024-02-07 18:02:20 +01:00
if ( hasDashboardAndPanel ) {
2024-01-02 10:05:58 +01:00
metadata . push ( {
label : 'Dashboard and panel' ,
value : (
2024-02-07 18:02:20 +01:00
< WithReturnButton
title = { rule . name }
component = {
< TextLink variant = "bodySmall" href = { makePanelLink ( dashboardUID , panelID ) } >
View panel
< / TextLink >
}
/ >
2024-01-02 10:05:58 +01:00
) ,
} ) ;
2024-02-07 18:02:20 +01:00
} else if ( hasDashboard ) {
2024-01-02 10:05:58 +01:00
metadata . push ( {
label : 'Dashboard' ,
value : (
2024-02-07 18:02:20 +01:00
< WithReturnButton
title = { rule . name }
component = {
< TextLink title = { rule . name } variant = "bodySmall" href = { makeDashboardLink ( dashboardUID ) } >
View dashboard
< / TextLink >
}
/ >
2024-01-02 10:05:58 +01:00
) ,
} ) ;
2023-07-06 13:16:47 +02:00
}
2024-01-02 10:05:58 +01:00
if ( interval ) {
metadata . push ( {
label : 'Evaluation interval' ,
value : < Text color = "primary" > Every { interval } < / Text > ,
} ) ;
}
if ( hasLabels ) {
metadata . push ( {
label : 'Labels' ,
/* TODO truncate number of labels, maybe build in to component? */
value : < AlertLabels labels = { labels } size = "sm" / > ,
} ) ;
}
return metadata ;
} ;
// TODO move somewhere else
export const createListFilterLink = ( values : Array < [ string , string ] > ) = > {
const params = new URLSearchParams ( [ [ 'search' , values . map ( ( [ key , value ] ) = > ` ${ key } :" ${ value } " ` ) . join ( ' ' ) ] ] ) ;
return createUrl ( ` /alerting/list? ` + params . toString ( ) ) ;
2023-07-06 13:16:47 +02:00
} ;
2024-01-02 10:05:58 +01:00
interface TitleProps {
name : string ;
2024-03-27 16:50:41 +01:00
paused? : boolean ;
2024-01-02 10:05:58 +01:00
// recording rules don't have a state
state? : PromAlertingRuleState ;
2024-02-07 18:02:20 +01:00
health? : RuleHealth ;
ruleType? : PromRuleType ;
2024-04-30 10:34:52 +02:00
ruleOrigin? : RulePluginOrigin ;
2023-07-06 13:16:47 +02:00
}
2024-04-30 10:34:52 +02:00
export const Title = ( { name , paused = false , state , health , ruleType , ruleOrigin } : TitleProps ) = > {
2024-02-07 18:02:20 +01:00
const styles = useStyles2 ( getStyles ) ;
const isRecordingRule = ruleType === PromRuleType . Recording ;
2024-01-02 10:05:58 +01:00
return (
2024-02-07 18:02:20 +01:00
< div className = { styles . title } >
< LinkButton variant = "secondary" icon = "angle-left" href = "/alerting/list" / >
2024-04-30 10:34:52 +02:00
{ ruleOrigin && < PluginOriginBadge pluginId = { ruleOrigin . pluginId } / > }
2024-02-07 18:02:20 +01:00
< Text variant = "h1" truncate >
{ name }
2023-07-20 12:59:42 +02:00
< / Text >
2024-03-27 16:50:41 +01:00
{ paused ? (
< PausedBadge / >
) : (
< >
{ /* recording rules won't have a state */ }
{ state && < StateBadge state = { state } health = { health } / > }
{ isRecordingRule && < RecordingBadge health = { health } / > }
< / >
) }
2024-02-07 18:02:20 +01:00
< / div >
2024-01-02 10:05:58 +01:00
) ;
} ;
2024-02-07 18:02:20 +01:00
export const isErrorHealth = ( health? : RuleHealth ) = > health === 'error' || health === 'err' ;
2024-01-02 10:05:58 +01:00
function useActiveTab ( ) : [ ActiveTab , ( tab : ActiveTab ) = > void ] {
const [ queryParams , setQueryParams ] = useQueryParams ( ) ;
const tabFromQuery = queryParams [ 'tab' ] ;
const activeTab = isValidTab ( tabFromQuery ) ? tabFromQuery : ActiveTab.Query ;
2023-07-06 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
const setActiveTab = ( tab : ActiveTab ) = > {
setQueryParams ( { tab } ) ;
} ;
return [ activeTab , setActiveTab ] ;
2023-07-06 13:16:47 +02:00
}
2024-01-02 10:05:58 +01:00
function isValidTab ( tab : UrlQueryValue ) : tab is ActiveTab {
const isString = typeof tab === 'string' ;
// @ts-ignore
return isString && Object . values ( ActiveTab ) . includes ( tab ) ;
}
function usePageNav ( rule : CombinedRule ) {
const [ activeTab , setActiveTab ] = useActiveTab ( ) ;
const { annotations , promRule } = rule ;
const summary = annotations [ Annotation . summary ] ;
const isAlertType = isAlertingRule ( promRule ) ;
const numberOfInstance = isAlertType ? ( promRule . alerts ? ? [ ] ) . length : undefined ;
2024-02-08 13:18:20 +01:00
const namespaceName = decodeGrafanaNamespace ( rule . namespace ) . name ;
2024-01-17 11:07:39 +02:00
const groupName = rule . group . name ;
2024-02-07 18:02:20 +01:00
const isGrafanaAlertRule = isGrafanaRulerRule ( rule . rulerRule ) && isAlertType ;
const isRecordingRuleType = isRecordingRule ( rule . promRule ) ;
2024-01-02 10:05:58 +01:00
const pageNav : NavModelItem = {
. . . defaultPageNav ,
text : rule.name ,
subTitle : summary ,
children : [
{
text : 'Query and conditions' ,
active : activeTab === ActiveTab . Query ,
onClick : ( ) = > {
setActiveTab ( ActiveTab . Query ) ;
} ,
} ,
{
text : 'Instances' ,
active : activeTab === ActiveTab . Instances ,
onClick : ( ) = > {
setActiveTab ( ActiveTab . Instances ) ;
} ,
tabCounter : numberOfInstance ,
2024-02-07 18:02:20 +01:00
hideFromTabs : isRecordingRuleType ,
2024-01-02 10:05:58 +01:00
} ,
{
text : 'History' ,
active : activeTab === ActiveTab . History ,
onClick : ( ) = > {
setActiveTab ( ActiveTab . History ) ;
} ,
2024-02-07 18:02:20 +01:00
// alert state history is only available for Grafana managed alert rules
hideFromTabs : ! isGrafanaAlertRule ,
2024-01-02 10:05:58 +01:00
} ,
{
text : 'Details' ,
active : activeTab === ActiveTab . Details ,
onClick : ( ) = > {
setActiveTab ( ActiveTab . Details ) ;
} ,
} ,
] ,
parentItem : {
2024-01-17 11:07:39 +02:00
text : groupName ,
2024-01-02 10:05:58 +01:00
url : createListFilterLink ( [
2024-01-17 11:07:39 +02:00
[ 'namespace' , namespaceName ] ,
[ 'group' , groupName ] ,
2024-01-02 10:05:58 +01:00
] ) ,
2024-02-08 13:18:20 +01:00
// @TODO support nested folders here
2024-01-02 10:05:58 +01:00
parentItem : {
2024-01-17 11:07:39 +02:00
text : namespaceName ,
url : createListFilterLink ( [ [ 'namespace' , namespaceName ] ] ) ,
2024-01-02 10:05:58 +01:00
} ,
} ,
} ;
return {
pageNav ,
activeTab ,
} ;
}
2023-07-06 13:16:47 +02:00
2024-02-07 18:02:20 +01:00
const getStyles = ( ) = > ( {
title : css ( {
display : 'flex' ,
alignItems : 'center' ,
gap : 8 ,
minWidth : 0 ,
} ) ,
} ) ;
2023-07-06 13:16:47 +02:00
export default RuleViewer ;