Files
grafana/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx

362 lines
11 KiB
TypeScript
Raw Normal View History

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';
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';
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';
import { defaultPageNav } from '../../RuleViewer';
import { PluginOriginBadge } from '../../plugins/PluginOriginBadge';
import { Annotation } from '../../utils/constants';
import { makeDashboardLink, makePanelLink } from '../../utils/misc';
import {
RulePluginOrigin,
getRulePluginOrigin,
isAlertingRule,
isFederatedRuleGroup,
isGrafanaRulerRule,
isGrafanaRulerRulePaused,
isRecordingRule,
} from '../../utils/rules';
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';
2024-01-23 15:04:12 +01:00
import { useAlertRulePageActions } from './Actions';
2024-01-02 10:05:58 +01:00
import { useDeleteModal } from './DeleteModal';
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';
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',
}
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);
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>();
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);
2024-01-02 10:05:58 +01:00
const isFederatedRule = isFederatedRuleGroup(rule.group);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
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;
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}
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">
{isPaused && <InfoPausedRule />}
2024-02-07 18:02:20 +01:00
{summary}
{/* 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>
</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
),
});
}
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());
};
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;
ruleOrigin?: RulePluginOrigin;
}
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" />
{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;
2024-01-02 10:05:58 +01:00
const setActiveTab = (tab: ActiveTab) => {
setQueryParams({ tab });
};
return [activeTab, setActiveTab];
}
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;
const namespaceName = decodeGrafanaNamespace(rule.namespace).name;
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: {
text: groupName,
2024-01-02 10:05:58 +01:00
url: createListFilterLink([
['namespace', namespaceName],
['group', groupName],
2024-01-02 10:05:58 +01:00
]),
// @TODO support nested folders here
2024-01-02 10:05:58 +01:00
parentItem: {
text: namespaceName,
url: createListFilterLink([['namespace', namespaceName]]),
2024-01-02 10:05:58 +01:00
},
},
};
return {
pageNav,
activeTab,
};
}
2024-02-07 18:02:20 +01:00
const getStyles = () => ({
title: css({
display: 'flex',
alignItems: 'center',
gap: 8,
minWidth: 0,
}),
});
export default RuleViewer;