2024-01-02 10:05:58 +01:00
import { isEmpty , truncate } from 'lodash' ;
import React from 'react' ;
2023-07-06 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
import { AppEvents , NavModelItem , UrlQueryValue } from '@grafana/data' ;
import { Alert , Button , Dropdown , LinkButton , Menu , Stack , TabContent , Text , TextLink } from '@grafana/ui' ;
import { PageInfoItem } from 'app/core/components/Page/types' ;
import { appEvents } from 'app/core/core' ;
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 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
import { defaultPageNav } from '../../../RuleViewer' ;
import { AlertRuleAction , useAlertRuleAbility } from '../../../hooks/useAbilities' ;
import { Annotation } from '../../../utils/constants' ;
import {
createShareLink ,
isLocalDevEnv ,
isOpenSourceEdition ,
makeDashboardLink ,
makePanelLink ,
makeRuleBasedSilenceLink ,
} from '../../../utils/misc' ;
2023-07-06 13:16:47 +02:00
import * as ruleId from '../../../utils/rule-id' ;
import { isAlertingRule , isFederatedRuleGroup , isGrafanaRulerRule } from '../../../utils/rules' ;
2024-01-02 10:05:58 +01:00
import { createUrl } from '../../../utils/url' ;
import { AlertLabels } from '../../AlertLabels' ;
2023-07-06 13:16:47 +02:00
import { AlertStateDot } from '../../AlertStateDot' ;
2024-01-02 10:05:58 +01:00
import { AlertingPageWrapper } from '../../AlertingPageWrapper' ;
import MoreButton from '../../MoreButton' ;
2023-07-06 13:16:47 +02:00
import { ProvisionedResource , ProvisioningAlert } from '../../Provisioning' ;
2024-01-02 10:05:58 +01:00
import { DeclareIncidentMenuItem } from '../../bridges/DeclareIncidentButton' ;
2024-01-17 11:07:39 +02:00
import { decodeGrafanaNamespace } from '../../expressions/util' ;
2024-01-02 10:05:58 +01:00
import { Details } from '../tabs/Details' ;
2023-07-06 13:16:47 +02:00
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
import { useDeleteModal } from './DeleteModal' ;
2023-07-06 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
type RuleViewerProps = {
rule : CombinedRule ;
identifier : RuleIdentifier ;
} ;
enum ActiveTab {
Query = 'query' ,
Instances = 'instances' ,
History = 'history' ,
Routing = 'routing' ,
Details = 'details' ,
2023-07-06 13:16:47 +02:00
}
2024-01-02 10:05:58 +01:00
const RuleViewer = ( { rule , identifier } : RuleViewerProps ) = > {
const { pageNav , activeTab } = usePageNav ( rule ) ;
const [ deleteModal , showDeleteModal ] = useDeleteModal ( ) ;
2023-08-08 20:36:38 +02:00
2024-01-02 10:05:58 +01:00
const [ editSupported , editAllowed ] = useAlertRuleAbility ( rule , AlertRuleAction . Update ) ;
const canEdit = editSupported && editAllowed ;
2023-08-08 20:36:38 +02:00
2024-01-02 10:05:58 +01:00
const [ deleteSupported , deleteAllowed ] = useAlertRuleAbility ( rule , AlertRuleAction . Delete ) ;
const canDelete = deleteSupported && deleteAllowed ;
2023-07-06 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
const [ duplicateSupported , duplicateAllowed ] = useAlertRuleAbility ( rule , AlertRuleAction . Duplicate ) ;
const canDuplicate = duplicateSupported && duplicateAllowed ;
2023-07-06 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
const [ silenceSupported , silenceAllowed ] = useAlertRuleAbility ( rule , AlertRuleAction . Silence ) ;
const canSilence = silenceSupported && silenceAllowed ;
2023-07-06 13:16:47 +02:00
2024-01-02 10:05:58 +01:00
const [ exportSupported , exportAllowed ] = useAlertRuleAbility ( rule , AlertRuleAction . ModifyExport ) ;
const canExport = exportSupported && exportAllowed ;
const promRule = rule . promRule ;
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 ) ;
/ * *
* Since Incident isn 't available as an open-source product we shouldn' t show it for Open - Source licenced editions of Grafana .
* We should show it in development mode
* /
const shouldShowDeclareIncidentButton = ! isOpenSourceEdition ( ) || isLocalDevEnv ( ) ;
const shareUrl = createShareLink ( rule . namespace . rulesSource , rule ) ;
const copyShareUrl = ( ) = > {
if ( navigator . clipboard ) {
navigator . clipboard . writeText ( shareUrl ) ;
appEvents . emit ( AppEvents . alertSuccess , [ 'URL copied to clipboard' ] ) ;
}
} ;
return (
< AlertingPageWrapper
pageNav = { pageNav }
navId = "alert-list"
isLoading = { false }
renderTitle = { ( title ) = > {
return < Title name = { title } state = { isAlertType ? promRule.state : undefined } / > ;
} }
actions = { [
canEdit && < EditButton key = "edit-action" identifier = { identifier } / > ,
< Dropdown
key = "more-actions"
overlay = {
< Menu >
{ canSilence && (
< Menu.Item
label = "Silence"
icon = "bell-slash"
url = { makeRuleBasedSilenceLink ( identifier . ruleSourceName , rule ) }
/ >
) }
{ shouldShowDeclareIncidentButton && < DeclareIncidentMenuItem title = { rule . name } url = { '' } / > }
{ canDuplicate && < Menu.Item label = "Duplicate" icon = "copy" / > }
< Menu.Divider / >
< Menu.Item label = "Copy link" icon = "share-alt" onClick = { copyShareUrl } / >
{ canExport && (
< Menu.Item
label = "Export"
icon = "download-alt"
childItems = { [
< Menu.Item key = "no-modifications" label = "Without modifications" icon = "file-blank" / > ,
< Menu.Item key = "with-modifications" label = "With modifications" icon = "file-alt" / > ,
] }
/ >
) }
{ canDelete && (
< >
< Menu.Divider / >
< Menu.Item label = "Delete" icon = "trash-alt" destructive onClick = { ( ) = > showDeleteModal ( rule ) } / >
< / >
) }
< / Menu >
}
>
< MoreButton size = "md" / >
< / Dropdown > ,
] }
info = { createMetadata ( rule ) }
>
< Stack direction = "column" gap = { 2 } >
{ /* actions */ }
< Stack direction = "column" gap = { 2 } >
2023-07-06 13:16:47 +02: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 10:05:58 +01: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 13:16:47 +02:00
< / TabContent >
< / Stack >
2024-01-02 10:05:58 +01:00
< / Stack >
{ deleteModal }
< / AlertingPageWrapper >
) ;
} ;
interface EditButtonProps {
identifier : RuleIdentifier ;
}
export const EditButton = ( { identifier } : EditButtonProps ) = > {
const returnTo = location . pathname + location . search ;
const ruleIdentifier = ruleId . stringifyIdentifier ( identifier ) ;
const editURL = createUrl ( ` /alerting/ ${ encodeURIComponent ( ruleIdentifier ) } /edit ` , { returnTo } ) ;
return (
< LinkButton variant = "secondary" icon = "pen" href = { editURL } >
Edit
< / LinkButton >
) ;
} ;
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 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 ;
// recording rules don't have a state
state? : PromAlertingRuleState ;
2023-07-06 13:16:47 +02:00
}
2024-01-02 10:05:58 +01: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 12:59:42 +02:00
< / Text >
2024-01-02 10:05:58 +01:00
{ /* recording rules won't have a state */ }
{ state && < StateBadge state = { state } / > }
< / div >
2023-07-06 13:16:47 +02:00
) ;
2024-01-02 10:05:58 +01:00
interface StateBadgeProps {
state : PromAlertingRuleState ;
2023-07-06 13:16:47 +02:00
}
2024-01-02 10:05:58 +01: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 13:16:47 +02:00
< AlertStateDot size = "md" state = { state } / >
2024-01-02 10:05:58 +01:00
< Text variant = "bodySmall" color = { textColor } >
{ stateLabel }
2023-07-20 12:59:42 +02:00
< / Text >
2023-07-06 13:16:47 +02:00
< / Stack >
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-01-17 11:07:39 +02:00
const namespaceName = decodeGrafanaNamespace ( rule . namespace ) ;
const groupName = rule . group . name ;
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 ,
} ,
{
text : 'History' ,
active : activeTab === ActiveTab . History ,
onClick : ( ) = > {
setActiveTab ( ActiveTab . History ) ;
} ,
} ,
{
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
] ) ,
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
export default RuleViewer ;