2024-01-02 17:05:58 +08:00
import { isEmpty , truncate } from 'lodash' ;
2024-01-23 22:04:12 +08:00
import React , { useState } from 'react' ;
2023-07-06 19:16:47 +08:00
2024-01-23 22:04:12 +08:00
import { NavModelItem , UrlQueryValue } from '@grafana/data' ;
import { Alert , Button , LinkButton , Stack , TabContent , Text , TextLink } from '@grafana/ui' ;
2024-01-02 17:05:58 +08:00
import { PageInfoItem } from 'app/core/components/Page/types' ;
import { useQueryParams } from 'app/core/hooks/useQueryParams' ;
import { CombinedRule , RuleIdentifier } from 'app/types/unified-alerting' ;
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto' ;
2023-07-06 19:16:47 +08:00
2024-01-02 17:05:58 +08:00
import { defaultPageNav } from '../../../RuleViewer' ;
import { Annotation } from '../../../utils/constants' ;
2024-01-23 22:04:12 +08:00
import { makeDashboardLink , makePanelLink } from '../../../utils/misc' ;
2023-07-06 19:16:47 +08:00
import { isAlertingRule , isFederatedRuleGroup , isGrafanaRulerRule } from '../../../utils/rules' ;
2024-01-02 17:05:58 +08:00
import { createUrl } from '../../../utils/url' ;
import { AlertLabels } from '../../AlertLabels' ;
2023-07-06 19:16:47 +08:00
import { AlertStateDot } from '../../AlertStateDot' ;
2024-01-02 17:05:58 +08:00
import { AlertingPageWrapper } from '../../AlertingPageWrapper' ;
2023-07-06 19:16:47 +08:00
import { ProvisionedResource , ProvisioningAlert } from '../../Provisioning' ;
2024-01-17 17:07:39 +08:00
import { decodeGrafanaNamespace } from '../../expressions/util' ;
2024-01-23 22:04:12 +08:00
import { RedirectToCloneRule } from '../../rules/CloneRule' ;
2024-01-02 17:05:58 +08:00
import { Details } from '../tabs/Details' ;
2023-07-06 19:16:47 +08:00
import { History } from '../tabs/History' ;
import { InstancesList } from '../tabs/Instances' ;
import { QueryResults } from '../tabs/Query' ;
import { Routing } from '../tabs/Routing' ;
2024-01-23 22:04:12 +08:00
import { useAlertRulePageActions } from './Actions' ;
2024-01-02 17:05:58 +08:00
import { useDeleteModal } from './DeleteModal' ;
2024-01-23 22:04:12 +08:00
import { useAlertRule } from './RuleContext' ;
2024-01-02 17:05:58 +08:00
enum ActiveTab {
Query = 'query' ,
Instances = 'instances' ,
History = 'history' ,
Routing = 'routing' ,
Details = 'details' ,
2023-07-06 19:16:47 +08:00
}
2024-01-23 22:04:12 +08:00
const RuleViewer = ( ) = > {
const { rule } = useAlertRule ( ) ;
2024-01-02 17:05:58 +08:00
const { pageNav , activeTab } = usePageNav ( rule ) ;
2023-08-09 02:36:38 +08:00
2024-01-23 22:04:12 +08: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 19:16:47 +08:00
2024-01-23 22:04:12 +08:00
const [ deleteModal , showDeleteModal ] = useDeleteModal ( ) ;
const actions = useAlertRulePageActions ( {
handleDuplicateRule : setDuplicateRuleIdentifier ,
handleDelete : showDeleteModal ,
} ) ;
2024-01-02 17:05:58 +08:00
const promRule = rule . promRule ;
const isAlertType = isAlertingRule ( promRule ) ;
2023-07-06 19:16:47 +08:00
2024-01-02 17:05:58 +08:00
const isFederatedRule = isFederatedRuleGroup ( rule . group ) ;
const isProvisioned = isGrafanaRulerRule ( rule . rulerRule ) && Boolean ( rule . rulerRule . grafana_alert . provenance ) ;
return (
< AlertingPageWrapper
pageNav = { pageNav }
navId = "alert-list"
isLoading = { false }
renderTitle = { ( title ) = > {
return < Title name = { title } state = { isAlertType ? promRule.state : undefined } / > ;
} }
2024-01-23 22:04:12 +08:00
actions = { actions }
2024-01-02 17:05:58 +08:00
info = { createMetadata ( rule ) }
>
< Stack direction = "column" gap = { 2 } >
{ /* actions */ }
< Stack direction = "column" gap = { 2 } >
2023-07-06 19:16:47 +08:00
{ /* alerts and notifications and stuff */ }
{ isFederatedRule && (
< Alert severity = "info" title = "This rule is part of a federated rule group." >
< Stack direction = "column" >
Federated rule groups are currently an experimental feature .
< Button fill = "text" icon = "book" >
< a href = "https://grafana.com/docs/metrics-enterprise/latest/tenant-management/tenant-federation/#cross-tenant-alerting-and-recording-rule-federation" >
Read documentation
< / a >
< / Button >
< / Stack >
< / Alert >
) }
{ isProvisioned && < ProvisioningAlert resource = { ProvisionedResource . AlertRule } / > }
{ /* tabs and tab content */ }
< TabContent >
2024-01-02 17:05:58 +08:00
{ 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 } / > }
2023-07-06 19:16:47 +08:00
< / TabContent >
< / Stack >
2024-01-02 17:05:58 +08:00
< / Stack >
{ deleteModal }
2024-01-23 22:04:12 +08:00
{ duplicateRuleIdentifier && (
< RedirectToCloneRule
redirectTo = { true }
identifier = { duplicateRuleIdentifier }
isProvisioned = { isProvisioned }
onDismiss = { ( ) = > setDuplicateRuleIdentifier ( undefined ) }
/ >
) }
2024-01-02 17:05:58 +08: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 ] ;
const hasPanel = dashboardUID && panelID ;
const hasDashboardWithoutPanel = dashboardUID && ! panelID ;
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 >
) ,
} ) ;
}
if ( hasPanel ) {
metadata . push ( {
label : 'Dashboard and panel' ,
value : (
< TextLink variant = "bodySmall" href = { makePanelLink ( dashboardUID , panelID ) } external >
View panel
< / TextLink >
) ,
} ) ;
} else if ( hasDashboardWithoutPanel ) {
metadata . push ( {
label : 'Dashboard' ,
value : (
< TextLink variant = "bodySmall" href = { makeDashboardLink ( dashboardUID ) } external >
View dashboard
< / TextLink >
) ,
} ) ;
2023-07-06 19:16:47 +08:00
}
2024-01-02 17:05:58 +08: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 19:16:47 +08:00
} ;
2024-01-02 17:05:58 +08:00
interface TitleProps {
name : string ;
// recording rules don't have a state
state? : PromAlertingRuleState ;
2023-07-06 19:16:47 +08:00
}
2024-01-02 17:05:58 +08:00
export const Title = ( { name , state } : TitleProps ) = > (
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , maxWidth : '100%' } } >
< LinkButton variant = "secondary" icon = "angle-left" href = "/alerting/list" / >
< Text element = "h1" truncate >
{ name }
2023-07-20 18:59:42 +08:00
< / Text >
2024-01-02 17:05:58 +08:00
{ /* recording rules won't have a state */ }
{ state && < StateBadge state = { state } / > }
< / div >
2023-07-06 19:16:47 +08:00
) ;
2024-01-02 17:05:58 +08:00
interface StateBadgeProps {
state : PromAlertingRuleState ;
2023-07-06 19:16:47 +08:00
}
2024-01-02 17:05:58 +08:00
// TODO move to separate component
const StateBadge = ( { state } : StateBadgeProps ) = > {
let stateLabel : string ;
let textColor : 'success' | 'error' | 'warning' ;
switch ( state ) {
case PromAlertingRuleState . Inactive :
textColor = 'success' ;
stateLabel = 'Normal' ;
break ;
case PromAlertingRuleState . Firing :
textColor = 'error' ;
stateLabel = 'Firing' ;
break ;
case PromAlertingRuleState . Pending :
textColor = 'warning' ;
stateLabel = 'Pending' ;
break ;
}
return (
< Stack direction = "row" gap = { 0.5 } >
2023-07-06 19:16:47 +08:00
< AlertStateDot size = "md" state = { state } / >
2024-01-02 17:05:58 +08:00
< Text variant = "bodySmall" color = { textColor } >
{ stateLabel }
2023-07-20 18:59:42 +08:00
< / Text >
2023-07-06 19:16:47 +08:00
< / Stack >
2024-01-02 17:05:58 +08: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 19:16:47 +08:00
2024-01-02 17:05:58 +08:00
const setActiveTab = ( tab : ActiveTab ) = > {
setQueryParams ( { tab } ) ;
} ;
return [ activeTab , setActiveTab ] ;
2023-07-06 19:16:47 +08:00
}
2024-01-02 17:05:58 +08: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-01-17 17:07:39 +08:00
const namespaceName = decodeGrafanaNamespace ( rule . namespace ) ;
const groupName = rule . group . name ;
2024-01-02 17:05:58 +08: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 ,
} ,
{
text : 'History' ,
active : activeTab === ActiveTab . History ,
onClick : ( ) = > {
setActiveTab ( ActiveTab . History ) ;
} ,
} ,
{
text : 'Details' ,
active : activeTab === ActiveTab . Details ,
onClick : ( ) = > {
setActiveTab ( ActiveTab . Details ) ;
} ,
} ,
] ,
parentItem : {
2024-01-17 17:07:39 +08:00
text : groupName ,
2024-01-02 17:05:58 +08:00
url : createListFilterLink ( [
2024-01-17 17:07:39 +08:00
[ 'namespace' , namespaceName ] ,
[ 'group' , groupName ] ,
2024-01-02 17:05:58 +08:00
] ) ,
parentItem : {
2024-01-17 17:07:39 +08:00
text : namespaceName ,
url : createListFilterLink ( [ [ 'namespace' , namespaceName ] ] ) ,
2024-01-02 17:05:58 +08:00
} ,
} ,
} ;
return {
pageNav ,
activeTab ,
} ;
}
2023-07-06 19:16:47 +08:00
export default RuleViewer ;