mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Detail view v2 (#77795)
This commit is contained in:
parent
f6f259f0b5
commit
e5227550c4
@ -1464,8 +1464,7 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:5381": [
|
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
[0, 0, 0, "Styles should be written using objects.", "2"]
|
||||||
[0, 0, 0, "Styles should be written using objects.", "3"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/components/Authorize.tsx:5381": [
|
"public/app/features/alerting/unified/components/Authorize.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
|
@ -26,6 +26,7 @@ export function PageTabs({ navItem }: Props) {
|
|||||||
counter={child.tabCounter}
|
counter={child.tabCounter}
|
||||||
href={child.url}
|
href={child.url}
|
||||||
suffix={child.tabSuffix}
|
suffix={child.tabSuffix}
|
||||||
|
onChangeTab={child.onClick}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,7 @@ import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
|||||||
|
|
||||||
export default function Admin(): JSX.Element {
|
export default function Admin(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<AlertmanagerPageWrapper pageId="alerting-admin" accessType="notification">
|
<AlertmanagerPageWrapper navId="alerting-admin" accessType="notification">
|
||||||
<AdminPageContents />
|
<AdminPageContents />
|
||||||
</AlertmanagerPageWrapper>
|
</AlertmanagerPageWrapper>
|
||||||
);
|
);
|
||||||
|
@ -91,7 +91,7 @@ const AlertGroups = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AlertGroupsPage = () => (
|
const AlertGroupsPage = () => (
|
||||||
<AlertmanagerPageWrapper pageId="groups" accessType="instance">
|
<AlertmanagerPageWrapper navId="groups" accessType="instance">
|
||||||
<AlertGroups />
|
<AlertGroups />
|
||||||
</AlertmanagerPageWrapper>
|
</AlertmanagerPageWrapper>
|
||||||
);
|
);
|
||||||
|
@ -78,7 +78,7 @@ const MuteTimingsPage = () => {
|
|||||||
const pageNav = useMuteTimingNavData();
|
const pageNav = useMuteTimingNavData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertmanagerPageWrapper pageId="am-routes" pageNav={pageNav} accessType="notification">
|
<AlertmanagerPageWrapper navId="am-routes" pageNav={pageNav} accessType="notification">
|
||||||
<MuteTimings />
|
<MuteTimings />
|
||||||
</AlertmanagerPageWrapper>
|
</AlertmanagerPageWrapper>
|
||||||
);
|
);
|
||||||
|
@ -329,7 +329,7 @@ function getActiveTabFromUrl(queryParams: UrlQueryMap): QueryParamValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NotificationPoliciesPage = () => (
|
const NotificationPoliciesPage = () => (
|
||||||
<AlertmanagerPageWrapper pageId="am-routes" accessType="notification">
|
<AlertmanagerPageWrapper navId="am-routes" accessType="notification">
|
||||||
<AmRoutes />
|
<AmRoutes />
|
||||||
</AlertmanagerPageWrapper>
|
</AlertmanagerPageWrapper>
|
||||||
);
|
);
|
||||||
|
@ -17,7 +17,7 @@ import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
|||||||
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
|
||||||
|
|
||||||
const ContactPoints = (_props: GrafanaRouteComponentProps): JSX.Element => (
|
const ContactPoints = (_props: GrafanaRouteComponentProps): JSX.Element => (
|
||||||
<AlertmanagerPageWrapper pageId="receivers" accessType="notification">
|
<AlertmanagerPageWrapper navId="receivers" accessType="notification">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact={true} path="/alerting/notifications" component={ContactPointsV2} />
|
<Route exact={true} path="/alerting/notifications" component={ContactPointsV2} />
|
||||||
<Route exact={true} path="/alerting/notifications/receivers/new" component={NewContactPoint} />
|
<Route exact={true} path="/alerting/notifications/receivers/new" component={NewContactPoint} />
|
||||||
|
@ -90,7 +90,7 @@ const RuleEditor = ({ match }: RuleEditorProps) => {
|
|||||||
}, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, id, identifier, loading]);
|
}, [canCreateCloudRules, canCreateGrafanaRules, canEditRules, copyFromIdentifier, id, identifier, loading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={getPageNav(identifier, type)}>
|
<AlertingPageWrapper isLoading={loading} navId="alert-list" pageNav={getPageNav(identifier, type)}>
|
||||||
{getContent()}
|
{getContent()}
|
||||||
</AlertingPageWrapper>
|
</AlertingPageWrapper>
|
||||||
);
|
);
|
||||||
|
@ -105,7 +105,7 @@ const RuleList = withErrorBoundary(
|
|||||||
return (
|
return (
|
||||||
// We don't want to show the Loading... indicator for the whole page.
|
// We don't want to show the Loading... indicator for the whole page.
|
||||||
// We show separate indicators for Grafana-managed and Cloud rules
|
// We show separate indicators for Grafana-managed and Cloud rules
|
||||||
<AlertingPageWrapper pageId="alert-list" isLoading={false}>
|
<AlertingPageWrapper navId="alert-list" isLoading={false}>
|
||||||
<RuleListErrors />
|
<RuleListErrors />
|
||||||
<RulesFilter onFilterCleared={onFilterCleared} />
|
<RulesFilter onFilterCleared={onFilterCleared} />
|
||||||
{!hasNoAlertRulesCreatedYet && (
|
{!hasNoAlertRulesCreatedYet && (
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { withErrorBoundary } from '@grafana/ui';
|
import { Alert, withErrorBoundary } from '@grafana/ui';
|
||||||
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||||
|
import { useCombinedRule } from './hooks/useCombinedRule';
|
||||||
|
import { getRuleIdFromPathname, parse as parseRuleId } from './utils/rule-id';
|
||||||
|
|
||||||
const DetailViewV1 = SafeDynamicImport(() => import('./components/rule-viewer/RuleViewer.v1'));
|
const DetailViewV1 = SafeDynamicImport(() => import('./components/rule-viewer/RuleViewer.v1'));
|
||||||
const DetailViewV2 = SafeDynamicImport(() => import('./components/rule-viewer/v2/RuleViewer.v2'));
|
const DetailViewV2 = React.lazy(() => import('./components/rule-viewer/v2/RuleViewer.v2'));
|
||||||
|
|
||||||
type RuleViewerProps = GrafanaRouteComponentProps<{
|
type RuleViewerProps = GrafanaRouteComponentProps<{
|
||||||
id: string;
|
id: string;
|
||||||
@ -17,10 +20,51 @@ type RuleViewerProps = GrafanaRouteComponentProps<{
|
|||||||
|
|
||||||
const newAlertDetailView = Boolean(config.featureToggles.alertingDetailsViewV2) === true;
|
const newAlertDetailView = Boolean(config.featureToggles.alertingDetailsViewV2) === true;
|
||||||
|
|
||||||
const RuleViewer = (props: RuleViewerProps): JSX.Element => (
|
const RuleViewer = (props: RuleViewerProps): JSX.Element => {
|
||||||
<AlertingPageWrapper>
|
return newAlertDetailView ? <RuleViewerV2Wrapper {...props} /> : <RuleViewerV1Wrapper {...props} />;
|
||||||
{newAlertDetailView ? <DetailViewV2 {...props} /> : <DetailViewV1 {...props} />}
|
};
|
||||||
|
|
||||||
|
export const defaultPageNav: NavModelItem = {
|
||||||
|
id: 'alert-rule-view',
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RuleViewerV1Wrapper = (props: RuleViewerProps) => <DetailViewV1 {...props} />;
|
||||||
|
|
||||||
|
const RuleViewerV2Wrapper = (props: RuleViewerProps) => {
|
||||||
|
const id = getRuleIdFromPathname(props.match.params);
|
||||||
|
const identifier = useMemo(() => {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('Rule ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseRuleId(id, true);
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
|
||||||
|
|
||||||
|
// TODO improve error handling here
|
||||||
|
if (error) {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Alert title={'Uh-oh'}>Something went wrong loading the rule</Alert>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<AlertingPageWrapper pageNav={defaultPageNav} navId="alert-list" isLoading={true}>
|
||||||
|
<></>
|
||||||
</AlertingPageWrapper>
|
</AlertingPageWrapper>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule) {
|
||||||
|
return <DetailViewV2 rule={rule} identifier={identifier} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export default withErrorBoundary(RuleViewer, { style: 'page' });
|
export default withErrorBoundary(RuleViewer, { style: 'page' });
|
||||||
|
@ -112,7 +112,7 @@ function SilencesPage() {
|
|||||||
const pageNav = useSilenceNavData();
|
const pageNav = useSilenceNavData();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertmanagerPageWrapper pageId="silences" pageNav={pageNav} accessType="instance">
|
<AlertmanagerPageWrapper navId="silences" pageNav={pageNav} accessType="instance">
|
||||||
<Silences />
|
<Silences />
|
||||||
</AlertmanagerPageWrapper>
|
</AlertmanagerPageWrapper>
|
||||||
);
|
);
|
||||||
|
@ -2,26 +2,28 @@ import { css } from '@emotion/css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { ComponentSize, Tooltip, useStyles2 } from '@grafana/ui';
|
import { ComponentSize, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import { GrafanaAlertState } from 'app/types/unified-alerting-dto';
|
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
const AlertStateDot = (props: DotStylesProps) => {
|
const AlertStateDot = (props: DotStylesProps) => {
|
||||||
const styles = useStyles2(getDotStyles, props);
|
const styles = useStyles2(getDotStyles, props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content={String(props.state)} placement="top">
|
<Stack direction="row" gap={0.5}>
|
||||||
<div className={styles.dot} />
|
<div className={styles.dot} />
|
||||||
</Tooltip>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DotStylesProps {
|
interface DotStylesProps {
|
||||||
state?: GrafanaAlertState;
|
state: PromAlertingRuleState;
|
||||||
|
includeState?: boolean;
|
||||||
size?: ComponentSize; // TODO support this
|
size?: ComponentSize; // TODO support this
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDotStyles = (theme: GrafanaTheme2, props: DotStylesProps) => {
|
const getDotStyles = (theme: GrafanaTheme2, props: DotStylesProps) => {
|
||||||
const size = theme.spacing(1.25);
|
const size = theme.spacing(1.25);
|
||||||
|
const outlineSize = `calc(${size} / 2.5)`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dot: css`
|
dot: css`
|
||||||
@ -31,21 +33,22 @@ const getDotStyles = (theme: GrafanaTheme2, props: DotStylesProps) => {
|
|||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
|
||||||
background-color: ${theme.colors.secondary.main};
|
background-color: ${theme.colors.secondary.main};
|
||||||
outline: solid calc(${size} / 2.5) ${theme.colors.secondary.transparent};
|
outline: solid ${outlineSize} ${theme.colors.secondary.transparent};
|
||||||
|
margin: ${outlineSize};
|
||||||
|
|
||||||
${props.state === GrafanaAlertState.Normal &&
|
${props.state === PromAlertingRuleState.Inactive &&
|
||||||
css`
|
css`
|
||||||
background-color: ${theme.colors.success.main};
|
background-color: ${theme.colors.success.main};
|
||||||
outline-color: ${theme.colors.success.transparent};
|
outline-color: ${theme.colors.success.transparent};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props.state === GrafanaAlertState.Pending &&
|
${props.state === PromAlertingRuleState.Pending &&
|
||||||
css`
|
css`
|
||||||
background-color: ${theme.colors.warning.main};
|
background-color: ${theme.colors.warning.main};
|
||||||
outline-color: ${theme.colors.warning.transparent};
|
outline-color: ${theme.colors.warning.transparent};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${props.state === GrafanaAlertState.Alerting &&
|
${props.state === PromAlertingRuleState.Firing &&
|
||||||
css`
|
css`
|
||||||
background-color: ${theme.colors.error.main};
|
background-color: ${theme.colors.error.main};
|
||||||
outline-color: ${theme.colors.error.transparent};
|
outline-color: ${theme.colors.error.transparent};
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { PropsWithChildren } from 'react';
|
import React, { PropsWithChildren } from 'react';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
import { PageProps } from 'app/core/components/Page/types';
|
||||||
|
|
||||||
import { AlertmanagerProvider, useAlertmanager } from '../state/AlertmanagerContext';
|
import { AlertmanagerProvider, useAlertmanager } from '../state/AlertmanagerContext';
|
||||||
|
|
||||||
@ -12,19 +12,15 @@ import { NoAlertManagerWarning } from './NoAlertManagerWarning';
|
|||||||
/**
|
/**
|
||||||
* This is the main alerting page wrapper, used by the alertmanager page wrapper and the alert rules list view
|
* This is the main alerting page wrapper, used by the alertmanager page wrapper and the alert rules list view
|
||||||
*/
|
*/
|
||||||
interface AlertingPageWrapperProps extends PropsWithChildren {
|
interface AlertingPageWrapperProps extends PageProps {
|
||||||
pageId?: string;
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
pageNav?: NavModelItem;
|
|
||||||
actions?: React.ReactNode;
|
|
||||||
}
|
}
|
||||||
export const AlertingPageWrapper = ({ children, pageId, pageNav, actions, isLoading }: AlertingPageWrapperProps) => {
|
|
||||||
return (
|
export const AlertingPageWrapper = ({ children, isLoading, ...rest }: AlertingPageWrapperProps) => (
|
||||||
<Page pageNav={pageNav} navId={pageId} actions={actions}>
|
<Page {...rest}>
|
||||||
<Page.Contents isLoading={isLoading}>{children}</Page.Contents>
|
<Page.Contents isLoading={isLoading}>{children}</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This wrapper is for pages that use the Alertmanager API
|
* This wrapper is for pages that use the Alertmanager API
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, textUtil } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Tooltip, useStyles2 } from '@grafana/ui';
|
import { TextLink, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { Annotation, annotationLabels } from '../utils/constants';
|
import { Annotation, annotationLabels } from '../utils/constants';
|
||||||
|
|
||||||
@ -44,9 +44,9 @@ const AnnotationValue = ({ annotationKey, value, valueLink }: Props) => {
|
|||||||
|
|
||||||
if (valueLink) {
|
if (valueLink) {
|
||||||
return (
|
return (
|
||||||
<a href={textUtil.sanitizeUrl(valueLink)} className={styles.link}>
|
<TextLink href={valueLink} external>
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</TextLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,9 +56,9 @@ const AnnotationValue = ({ annotationKey, value, valueLink }: Props) => {
|
|||||||
|
|
||||||
if (needsExternalLink) {
|
if (needsExternalLink) {
|
||||||
return (
|
return (
|
||||||
<a href={textUtil.sanitizeUrl(value)} target="__blank" className={styles.link}>
|
<TextLink href={value} external>
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</TextLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,8 +69,4 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
well: css`
|
well: css`
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
`,
|
`,
|
||||||
link: css`
|
|
||||||
word-break: break-all;
|
|
||||||
color: ${theme.colors.primary.text};
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
@ -5,13 +5,18 @@ import { Icon, IconName, useStyles2, Text, Stack } from '@grafana/ui';
|
|||||||
|
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
|
direction?: 'row' | 'column';
|
||||||
color?: ComponentProps<typeof Text>['color'];
|
color?: ComponentProps<typeof Text>['color'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const MetaText = ({ children, icon, color = 'secondary', ...rest }: Props) => {
|
const MetaText = ({ children, icon, color = 'secondary', direction = 'row', ...rest }: Props) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const interactive = typeof rest.onClick === 'function';
|
const interactive = typeof rest.onClick === 'function';
|
||||||
|
|
||||||
|
const rowDirection = direction === 'row';
|
||||||
|
const alignItems = rowDirection ? 'center' : 'flex-start';
|
||||||
|
const gap = rowDirection ? 0.5 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx({
|
className={cx({
|
||||||
@ -21,7 +26,7 @@ const MetaText = ({ children, icon, color = 'secondary', ...rest }: Props) => {
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<Text variant="bodySmall" color={color}>
|
<Text variant="bodySmall" color={color}>
|
||||||
<Stack direction="row" alignItems="center" gap={0.5} wrap={'wrap'}>
|
<Stack direction={direction} alignItems={alignItems} gap={gap} wrap={'wrap'}>
|
||||||
{icon && <Icon size="sm" name={icon} />}
|
{icon && <Icon size="sm" name={icon} />}
|
||||||
{children}
|
{children}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { FC } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Button, LinkButton, Tooltip } from '@grafana/ui';
|
import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||||
@ -8,11 +8,11 @@ import { createBridgeURL } from '../PluginBridge';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
severity?: 'minor' | 'major' | 'critical';
|
severity?: 'minor' | 'major' | 'critical' | '';
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeclareIncident: FC<Props> = ({ title = '', severity = '', url = '' }) => {
|
export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: Props) => {
|
||||||
const bridgeURL = createBridgeURL(SupportedPlugin.Incident, '/incidents/declare', { title, severity, url });
|
const bridgeURL = createBridgeURL(SupportedPlugin.Incident, '/incidents/declare', { title, severity, url });
|
||||||
|
|
||||||
const { loading, installed, settings } = usePluginBridge(SupportedPlugin.Incident);
|
const { loading, installed, settings } = usePluginBridge(SupportedPlugin.Incident);
|
||||||
@ -39,3 +39,21 @@ export const DeclareIncident: FC<Props> = ({ title = '', severity = '', url = ''
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }: Props) => {
|
||||||
|
const bridgeURL = createBridgeURL(SupportedPlugin.Incident, '/incidents/declare', { title, severity, url });
|
||||||
|
|
||||||
|
const { loading, installed, settings } = usePluginBridge(SupportedPlugin.Incident);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading === true && <Menu.Item label="Declare incident" icon="fire" disabled />}
|
||||||
|
{installed === false && (
|
||||||
|
<Tooltip content={'Grafana Incident is not installed or is not configured correctly'}>
|
||||||
|
<Menu.Item label="Declare incident" icon="fire" disabled />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{settings && <Menu.Item label="Declare incident" url={bridgeURL} icon="fire" />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -66,7 +66,7 @@ export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps)
|
|||||||
if (!alertRule && !loading && !loadingBuildInfo) {
|
if (!alertRule && !loading && !loadingBuildInfo) {
|
||||||
// alert rule does not exist
|
// alert rule does not exist
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={{ text: 'Modify export' }}>
|
<AlertingPageWrapper isLoading={loading} navId="alert-list" pageNav={{ text: 'Modify export' }}>
|
||||||
<Alert
|
<Alert
|
||||||
title="Cannot load the rule. The rule does not exist"
|
title="Cannot load the rule. The rule does not exist"
|
||||||
buttonContent="Go back to alert list"
|
buttonContent="Go back to alert list"
|
||||||
@ -79,7 +79,7 @@ export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps)
|
|||||||
if (alertRule && !isGrafanaRulerRule(alertRule.rule)) {
|
if (alertRule && !isGrafanaRulerRule(alertRule.rule)) {
|
||||||
// alert rule exists but is not a grafana-managed rule
|
// alert rule exists but is not a grafana-managed rule
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper isLoading={loading} pageId="alert-list" pageNav={{ text: 'Modify export' }}>
|
<AlertingPageWrapper isLoading={loading} navId="alert-list" pageNav={{ text: 'Modify export' }}>
|
||||||
<Alert
|
<Alert
|
||||||
title="This rule is not a Grafana-managed alert rule"
|
title="This rule is not a Grafana-managed alert rule"
|
||||||
buttonContent="Go back to alert list"
|
buttonContent="Go back to alert list"
|
||||||
@ -92,7 +92,7 @@ export default function GrafanaModifyExport({ match }: GrafanaModifyExportProps)
|
|||||||
return (
|
return (
|
||||||
<AlertingPageWrapper
|
<AlertingPageWrapper
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
pageId="alert-list"
|
navId="alert-list"
|
||||||
pageNav={{
|
pageNav={{
|
||||||
text: 'Modify export',
|
text: 'Modify export',
|
||||||
subTitle:
|
subTitle:
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { produce } from 'immer';
|
import React, { useMemo } from 'react';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useToggle } from 'react-use';
|
||||||
import { useObservable, useToggle } from 'react-use';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, LoadingState, PanelData, RelativeTimeRange } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { config, isFetchError } from '@grafana/runtime';
|
import { isFetchError } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@ -12,27 +11,24 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
LoadingPlaceholder,
|
LoadingPlaceholder,
|
||||||
Stack,
|
|
||||||
VerticalGroup,
|
|
||||||
useStyles2,
|
useStyles2,
|
||||||
|
VerticalGroup,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../../core/constants';
|
||||||
import { AlertQuery, GrafanaRuleDefinition } from '../../../../../types/unified-alerting-dto';
|
import { GrafanaRuleDefinition } from '../../../../../types/unified-alerting-dto';
|
||||||
import { GrafanaRuleQueryViewer, QueryPreview } from '../../GrafanaRuleQueryViewer';
|
|
||||||
import { useAlertQueriesStatus } from '../../hooks/useAlertQueriesStatus';
|
|
||||||
import { useCombinedRule } from '../../hooks/useCombinedRule';
|
import { useCombinedRule } from '../../hooks/useCombinedRule';
|
||||||
import { AlertingQueryRunner } from '../../state/AlertingQueryRunner';
|
|
||||||
import { useCleanAnnotations } from '../../utils/annotations';
|
import { useCleanAnnotations } from '../../utils/annotations';
|
||||||
import { getRulesSourceByName } from '../../utils/datasource';
|
import { getRulesSourceByName } from '../../utils/datasource';
|
||||||
import { alertRuleToQueries } from '../../utils/query';
|
|
||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { AlertLabels } from '../AlertLabels';
|
import { AlertLabels } from '../AlertLabels';
|
||||||
import { DetailsField } from '../DetailsField';
|
import { DetailsField } from '../DetailsField';
|
||||||
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
import { ProvisionedResource, ProvisioningAlert } from '../Provisioning';
|
||||||
import { RuleViewerLayout, RuleViewerLayoutContent } from '../rule-viewer/RuleViewerLayout';
|
import { RuleViewerLayout } from '../rule-viewer/RuleViewerLayout';
|
||||||
import { RuleDetailsActionButtons } from '../rules/RuleDetailsActionButtons';
|
import { RuleDetailsActionButtons } from '../rules/RuleDetailsActionButtons';
|
||||||
import { RuleDetailsAnnotations } from '../rules/RuleDetailsAnnotations';
|
import { RuleDetailsAnnotations } from '../rules/RuleDetailsAnnotations';
|
||||||
import { RuleDetailsDataSources } from '../rules/RuleDetailsDataSources';
|
import { RuleDetailsDataSources } from '../rules/RuleDetailsDataSources';
|
||||||
@ -42,6 +38,8 @@ import { RuleDetailsMatchingInstances } from '../rules/RuleDetailsMatchingInstan
|
|||||||
import { RuleHealth } from '../rules/RuleHealth';
|
import { RuleHealth } from '../rules/RuleHealth';
|
||||||
import { RuleState } from '../rules/RuleState';
|
import { RuleState } from '../rules/RuleState';
|
||||||
|
|
||||||
|
import { QueryResults } from './tabs/Query';
|
||||||
|
|
||||||
type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>;
|
type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>;
|
||||||
|
|
||||||
const errorMessage = 'Could not find data source for rule';
|
const errorMessage = 'Could not find data source for rule';
|
||||||
@ -63,58 +61,8 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
|
|
||||||
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
|
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
|
||||||
|
|
||||||
const runner = useMemo(() => new AlertingQueryRunner(), []);
|
|
||||||
const data = useObservable(runner.get());
|
|
||||||
const queries = useMemo(() => alertRuleToQueries(rule), [rule]);
|
|
||||||
const annotations = useCleanAnnotations(rule?.annotations || {});
|
const annotations = useCleanAnnotations(rule?.annotations || {});
|
||||||
|
|
||||||
const [evaluationTimeRanges, setEvaluationTimeRanges] = useState<Record<string, RelativeTimeRange>>({});
|
|
||||||
|
|
||||||
const { allDataSourcesAvailable } = useAlertQueriesStatus(queries);
|
|
||||||
|
|
||||||
const onRunQueries = useCallback(() => {
|
|
||||||
if (queries.length > 0 && allDataSourcesAvailable) {
|
|
||||||
const evalCustomizedQueries = queries.map<AlertQuery>((q) => ({
|
|
||||||
...q,
|
|
||||||
relativeTimeRange: evaluationTimeRanges[q.refId] ?? q.relativeTimeRange,
|
|
||||||
}));
|
|
||||||
let condition;
|
|
||||||
if (rule && isGrafanaRulerRule(rule.rulerRule)) {
|
|
||||||
condition = rule.rulerRule.grafana_alert.condition;
|
|
||||||
}
|
|
||||||
runner.run(evalCustomizedQueries, condition ?? 'A');
|
|
||||||
}
|
|
||||||
}, [queries, evaluationTimeRanges, runner, allDataSourcesAvailable, rule]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const alertQueries = alertRuleToQueries(rule);
|
|
||||||
const defaultEvalTimeRanges = Object.fromEntries(
|
|
||||||
alertQueries.map((q) => [q.refId, q.relativeTimeRange ?? { from: 0, to: 0 }])
|
|
||||||
);
|
|
||||||
|
|
||||||
setEvaluationTimeRanges(defaultEvalTimeRanges);
|
|
||||||
}, [rule]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (allDataSourcesAvailable && expandQuery) {
|
|
||||||
onRunQueries();
|
|
||||||
}
|
|
||||||
}, [onRunQueries, allDataSourcesAvailable, expandQuery]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => runner.destroy();
|
|
||||||
}, [runner]);
|
|
||||||
|
|
||||||
const onQueryTimeRangeChange = useCallback(
|
|
||||||
(refId: string, timeRange: RelativeTimeRange) => {
|
|
||||||
const newEvalTimeRanges = produce(evaluationTimeRanges, (draft) => {
|
|
||||||
draft[refId] = timeRange;
|
|
||||||
});
|
|
||||||
setEvaluationTimeRanges(newEvalTimeRanges);
|
|
||||||
},
|
|
||||||
[evaluationTimeRanges, setEvaluationTimeRanges]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!identifier?.ruleSourceName) {
|
if (!identifier?.ruleSourceName) {
|
||||||
return (
|
return (
|
||||||
<RuleViewerLayout title={pageTitle}>
|
<RuleViewerLayout title={pageTitle}>
|
||||||
@ -160,7 +108,17 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RuleViewerLayout
|
||||||
|
wrapInContent={false}
|
||||||
|
title={pageTitle}
|
||||||
|
renderTitle={() => (
|
||||||
|
<Stack direction="row" alignItems="flex-start" gap={1}>
|
||||||
|
<Icon name="bell" size="xl" />
|
||||||
|
<Text variant="h3">{rule.name}</Text>
|
||||||
|
<RuleState rule={rule} isCreating={false} isDeleting={false} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
>
|
||||||
{isFederatedRule && (
|
{isFederatedRule && (
|
||||||
<Alert severity="info" title="This rule is part of a federated rule group.">
|
<Alert severity="info" title="This rule is part of a federated rule group.">
|
||||||
<VerticalGroup>
|
<VerticalGroup>
|
||||||
@ -174,14 +132,8 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.AlertRule} />}
|
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.AlertRule} />}
|
||||||
<RuleViewerLayoutContent>
|
<>
|
||||||
<div>
|
|
||||||
<Stack direction="row" alignItems="center" gap={1}>
|
|
||||||
<Icon name="bell" size="lg" /> <span className={styles.title}>{rule.name}</span>
|
|
||||||
</Stack>
|
|
||||||
<RuleState rule={rule} isCreating={false} isDeleting={false} />
|
|
||||||
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} isViewMode={true} />
|
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} isViewMode={true} />
|
||||||
</div>
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div className={styles.leftSide}>
|
<div className={styles.leftSide}>
|
||||||
{rule.promRule && (
|
{rule.promRule && (
|
||||||
@ -207,57 +159,25 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
<DetailsField label="Matching instances" horizontal={true}>
|
||||||
<RuleDetailsMatchingInstances
|
<RuleDetailsMatchingInstances
|
||||||
rule={rule}
|
rule={rule}
|
||||||
pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }}
|
pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }}
|
||||||
enableFiltering
|
enableFiltering
|
||||||
/>
|
/>
|
||||||
|
</DetailsField>
|
||||||
</div>
|
</div>
|
||||||
</RuleViewerLayoutContent>
|
</>
|
||||||
<Collapse
|
<Collapse
|
||||||
label="Query & Results"
|
label="Query & Results"
|
||||||
isOpen={expandQuery}
|
isOpen={expandQuery}
|
||||||
onToggle={setExpandQuery}
|
onToggle={setExpandQuery}
|
||||||
loading={data && isLoading(data)}
|
|
||||||
collapsible={true}
|
collapsible={true}
|
||||||
className={styles.collapse}
|
className={styles.collapse}
|
||||||
>
|
>
|
||||||
{isGrafanaRulerRule(rule.rulerRule) && !isFederatedRule && (
|
{expandQuery && <QueryResults rule={rule} />}
|
||||||
<GrafanaRuleQueryViewer
|
|
||||||
condition={rule.rulerRule.grafana_alert.condition}
|
|
||||||
queries={queries}
|
|
||||||
evalDataByQuery={data}
|
|
||||||
evalTimeRanges={evaluationTimeRanges}
|
|
||||||
onTimeRangeChange={onQueryTimeRangeChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isGrafanaRulerRule(rule.rulerRule) && !isFederatedRule && data && Object.keys(data).length > 0 && (
|
|
||||||
<div className={styles.queries}>
|
|
||||||
{queries.map((query) => {
|
|
||||||
return (
|
|
||||||
<QueryPreview
|
|
||||||
key={query.refId}
|
|
||||||
refId={query.refId}
|
|
||||||
model={query.model}
|
|
||||||
dataSource={Object.values(config.datasources).find((ds) => ds.uid === query.datasourceUid)}
|
|
||||||
queryData={data[query.refId]}
|
|
||||||
relativeTimeRange={query.relativeTimeRange}
|
|
||||||
evalTimeRange={evaluationTimeRanges[query.refId]}
|
|
||||||
onEvalTimeRangeChange={(timeRange) => onQueryTimeRangeChange(query.refId, timeRange)}
|
|
||||||
isAlertCondition={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!isFederatedRule && !allDataSourcesAvailable && (
|
|
||||||
<Alert title="Query not available" severity="warning" className={styles.queryWarning}>
|
|
||||||
Cannot display the query preview. Some of the data sources used in the queries are not available.
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</>
|
</RuleViewerLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,10 +192,6 @@ function GrafanaRuleUID({ rule }: { rule: GrafanaRuleDefinition }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLoading(data: Record<string, PanelData>): boolean {
|
|
||||||
return !!Object.values(data).find((d) => d.state === LoadingState.Loading);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
errorMessage: css`
|
errorMessage: css`
|
||||||
|
@ -4,10 +4,12 @@ import React from 'react';
|
|||||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
import { PageProps } from 'app/core/components/Page/types';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode | React.ReactNode[];
|
children: React.ReactNode | React.ReactNode[];
|
||||||
title: string;
|
title: string;
|
||||||
|
renderTitle?: PageProps['renderTitle'];
|
||||||
wrapInContent?: boolean;
|
wrapInContent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,11 +19,11 @@ const defaultPageNav: Partial<NavModelItem> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function RuleViewerLayout(props: Props): JSX.Element | null {
|
export function RuleViewerLayout(props: Props): JSX.Element | null {
|
||||||
const { wrapInContent = true, children, title } = props;
|
const { wrapInContent = true, children, title, renderTitle } = props;
|
||||||
const styles = useStyles2(getPageStyles);
|
const styles = useStyles2(getPageStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page pageNav={{ ...defaultPageNav, text: title }} navId="alert-list">
|
<Page pageNav={{ ...defaultPageNav, text: title }} renderTitle={renderTitle} navId="alert-list">
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<div className={styles.content}>{wrapInContent ? <RuleViewerLayoutContent {...props} /> : children}</div>
|
<div className={styles.content}>{wrapInContent ? <RuleViewerLayoutContent {...props} /> : children}</div>
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
|
@ -0,0 +1,165 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { formatDistanceToNowStrict } from 'date-fns';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Text, Stack, useStyles2, ClipboardButton, TextLink } from '@grafana/ui';
|
||||||
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
import { Annotations } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { isGrafanaRulerRule, isRecordingRulerRule } from '../../../utils/rules';
|
||||||
|
import { MetaText } from '../../MetaText';
|
||||||
|
import { Tokenize } from '../../Tokenize';
|
||||||
|
|
||||||
|
interface DetailsProps {
|
||||||
|
rule: CombinedRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RuleType {
|
||||||
|
GrafanaManagedAlertRule = 'Grafana-managed alert rule',
|
||||||
|
CloudAlertRule = 'Cloud alert rule',
|
||||||
|
CloudRecordingRule = 'Cloud recording rule',
|
||||||
|
}
|
||||||
|
|
||||||
|
const Details = ({ rule }: DetailsProps) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
let ruleType: RuleType;
|
||||||
|
|
||||||
|
if (isGrafanaRulerRule(rule.rulerRule)) {
|
||||||
|
ruleType = RuleType.GrafanaManagedAlertRule;
|
||||||
|
} else if (isRecordingRulerRule(rule.rulerRule)) {
|
||||||
|
ruleType = RuleType.CloudRecordingRule;
|
||||||
|
} else {
|
||||||
|
// probably not the greatest assumption
|
||||||
|
ruleType = RuleType.CloudAlertRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
const evaluationDuration = rule.promRule?.evaluationTime;
|
||||||
|
const evaluationTimestamp = rule.promRule?.lastEvaluation;
|
||||||
|
|
||||||
|
const copyRuleUID = useCallback(() => {
|
||||||
|
if (isGrafanaRulerRule(rule.rulerRule)) {
|
||||||
|
return rule.rulerRule.grafana_alert.uid;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [rule.rulerRule]);
|
||||||
|
|
||||||
|
const annotations: Annotations | undefined = !isRecordingRulerRule(rule.rulerRule)
|
||||||
|
? rule.annotations ?? []
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="column" gap={3}>
|
||||||
|
<div className={styles.metadataWrapper}>
|
||||||
|
{/* type and identifier (optional) */}
|
||||||
|
<MetaText direction="column">
|
||||||
|
Rule type
|
||||||
|
<Text color="primary">{ruleType}</Text>
|
||||||
|
</MetaText>
|
||||||
|
<MetaText direction="column">
|
||||||
|
{isGrafanaRulerRule(rule.rulerRule) && (
|
||||||
|
<>
|
||||||
|
Rule Identifier
|
||||||
|
<Stack direction="row" alignItems="center" gap={0.5}>
|
||||||
|
<Text color="primary">
|
||||||
|
{rule.rulerRule.grafana_alert.uid}
|
||||||
|
<ClipboardButton fill="text" variant="secondary" icon="copy" size="sm" getText={copyRuleUID} />
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MetaText>
|
||||||
|
|
||||||
|
{/* evaluation duration and pending period */}
|
||||||
|
<MetaText direction="column">
|
||||||
|
{evaluationDuration && (
|
||||||
|
<>
|
||||||
|
Last evaluation
|
||||||
|
{evaluationTimestamp && evaluationDuration && (
|
||||||
|
<span>
|
||||||
|
<Text color="primary">{formatDistanceToNowStrict(new Date(evaluationTimestamp))} ago</Text>, took{' '}
|
||||||
|
<Text color="primary">{evaluationDuration}ms</Text>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MetaText>
|
||||||
|
<MetaText direction="column">
|
||||||
|
{!isRecordingRulerRule(rule.rulerRule) && (
|
||||||
|
<>
|
||||||
|
Pending period
|
||||||
|
<Text color="primary">{rule.rulerRule?.for ?? '0s'}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MetaText>
|
||||||
|
|
||||||
|
{/* nodata and execution error state mapping */}
|
||||||
|
{isGrafanaRulerRule(rule.rulerRule) && (
|
||||||
|
<>
|
||||||
|
<MetaText direction="column">
|
||||||
|
Alert state if no data or all values are null
|
||||||
|
<Text color="primary">{rule.rulerRule.grafana_alert.no_data_state}</Text>
|
||||||
|
</MetaText>
|
||||||
|
<MetaText direction="column">
|
||||||
|
Alert state if execution error or timeout
|
||||||
|
<Text color="primary">{rule.rulerRule.grafana_alert.exec_err_state}</Text>
|
||||||
|
</MetaText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* annotations go here */}
|
||||||
|
{annotations && (
|
||||||
|
<>
|
||||||
|
<Text variant="h4">Annotations</Text>
|
||||||
|
{Object.keys(annotations).length === 0 ? (
|
||||||
|
<Text variant="bodySmall" color="secondary" italic>
|
||||||
|
No annotations
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<div className={styles.metadataWrapper}>
|
||||||
|
{Object.entries(annotations).map(([name, value]) => (
|
||||||
|
<MetaText direction="column" key={name}>
|
||||||
|
{name}
|
||||||
|
<AnnotationValue value={value} />
|
||||||
|
</MetaText>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AnnotationValueProps {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnnotationValue({ value }: AnnotationValueProps) {
|
||||||
|
const needsExternalLink = value && value.startsWith('http');
|
||||||
|
const tokenizeValue = <Tokenize input={value} delimiter={['{{', '}}']} />;
|
||||||
|
|
||||||
|
if (needsExternalLink) {
|
||||||
|
return (
|
||||||
|
<TextLink variant="bodySmall" href={value} external>
|
||||||
|
{value}
|
||||||
|
</TextLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text color="primary">{tokenizeValue}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
metadataWrapper: css({
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'auto auto',
|
||||||
|
rowGap: theme.spacing(3),
|
||||||
|
columnGap: theme.spacing(12),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export { Details };
|
@ -1,5 +1,40 @@
|
|||||||
import React from 'react';
|
import React, { lazy, Suspense } from 'react';
|
||||||
|
|
||||||
const History = () => <>History</>;
|
import { config } from '@grafana/runtime';
|
||||||
|
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { StateHistoryImplementation } from '../../../hooks/useStateHistoryModal';
|
||||||
|
|
||||||
|
const AnnotationsStateHistory = lazy(() => import('../../../components/rules/state-history/StateHistory'));
|
||||||
|
const LokiStateHistory = lazy(() => import('../../../components/rules/state-history/LokiStateHistory'));
|
||||||
|
|
||||||
|
interface HistoryProps {
|
||||||
|
rule: RulerGrafanaRuleDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
const History = ({ rule }: HistoryProps) => {
|
||||||
|
// can be "loki", "multiple" or "annotations"
|
||||||
|
const stateHistoryBackend = config.unifiedAlerting.alertStateHistoryBackend;
|
||||||
|
// can be "loki" or "annotations"
|
||||||
|
const stateHistoryPrimary = config.unifiedAlerting.alertStateHistoryPrimary;
|
||||||
|
|
||||||
|
// if "loki" is either the backend or the primary, show the new state history implementation
|
||||||
|
const usingNewAlertStateHistory = [stateHistoryBackend, stateHistoryPrimary].some(
|
||||||
|
(implementation) => implementation === StateHistoryImplementation.Loki
|
||||||
|
);
|
||||||
|
const implementation = usingNewAlertStateHistory
|
||||||
|
? StateHistoryImplementation.Loki
|
||||||
|
: StateHistoryImplementation.Annotations;
|
||||||
|
|
||||||
|
const ruleID = rule.grafana_alert.id ?? '';
|
||||||
|
const ruleUID = rule.grafana_alert.uid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={'Loading...'}>
|
||||||
|
{implementation === StateHistoryImplementation.Loki && <LokiStateHistory ruleUID={ruleUID} />}
|
||||||
|
{implementation === StateHistoryImplementation.Annotations && <AnnotationsStateHistory alertId={ruleID} />}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export { History };
|
export { History };
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const InstancesList = () => <>Instances</>;
|
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
|
||||||
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { RuleDetailsMatchingInstances } from '../../rules/RuleDetailsMatchingInstances';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
rule: CombinedRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstancesList = ({ rule }: Props) => (
|
||||||
|
<RuleDetailsMatchingInstances
|
||||||
|
rule={rule}
|
||||||
|
pagination={{ itemsPerPage: DEFAULT_PER_PAGE_PAGINATION }}
|
||||||
|
enableFiltering
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
export { InstancesList };
|
export { InstancesList };
|
||||||
|
@ -1,5 +1,132 @@
|
|||||||
import React from 'react';
|
import { produce } from 'immer';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
const QueryResults = () => <>Query results</>;
|
import { LoadingState, PanelData, RelativeTimeRange } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { Alert } from '@grafana/ui';
|
||||||
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
import { AlertQuery } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { GrafanaRuleQueryViewer, QueryPreview } from '../../../GrafanaRuleQueryViewer';
|
||||||
|
import { useAlertQueriesStatus } from '../../../hooks/useAlertQueriesStatus';
|
||||||
|
import { AlertingQueryRunner } from '../../../state/AlertingQueryRunner';
|
||||||
|
import { alertRuleToQueries } from '../../../utils/query';
|
||||||
|
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../../utils/rules';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
rule: CombinedRule;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QueryResults = ({ rule }: Props) => {
|
||||||
|
const [evaluationTimeRanges, setEvaluationTimeRanges] = useState<Record<string, RelativeTimeRange>>({});
|
||||||
|
|
||||||
|
const runner = useMemo(() => new AlertingQueryRunner(), []);
|
||||||
|
const data = useObservable(runner.get());
|
||||||
|
const loadingData = isLoading(data);
|
||||||
|
|
||||||
|
const queries = useMemo(() => alertRuleToQueries(rule), [rule]);
|
||||||
|
|
||||||
|
const { allDataSourcesAvailable } = useAlertQueriesStatus(queries);
|
||||||
|
|
||||||
|
const onRunQueries = useCallback(() => {
|
||||||
|
if (queries.length > 0 && allDataSourcesAvailable) {
|
||||||
|
const evalCustomizedQueries = queries.map<AlertQuery>((q) => ({
|
||||||
|
...q,
|
||||||
|
relativeTimeRange: evaluationTimeRanges[q.refId] ?? q.relativeTimeRange,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let condition;
|
||||||
|
if (rule && isGrafanaRulerRule(rule.rulerRule)) {
|
||||||
|
condition = rule.rulerRule.grafana_alert.condition;
|
||||||
|
}
|
||||||
|
runner.run(evalCustomizedQueries, condition ?? 'A');
|
||||||
|
}
|
||||||
|
}, [queries, allDataSourcesAvailable, rule, runner, evaluationTimeRanges]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const alertQueries = alertRuleToQueries(rule);
|
||||||
|
const defaultEvalTimeRanges = Object.fromEntries(
|
||||||
|
alertQueries.map((q) => [q.refId, q.relativeTimeRange ?? { from: 0, to: 0 }])
|
||||||
|
);
|
||||||
|
|
||||||
|
setEvaluationTimeRanges(defaultEvalTimeRanges);
|
||||||
|
}, [rule]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (allDataSourcesAvailable) {
|
||||||
|
onRunQueries();
|
||||||
|
}
|
||||||
|
}, [allDataSourcesAvailable, onRunQueries]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => runner.destroy();
|
||||||
|
}, [runner]);
|
||||||
|
|
||||||
|
const onQueryTimeRangeChange = useCallback(
|
||||||
|
(refId: string, timeRange: RelativeTimeRange) => {
|
||||||
|
const newEvalTimeRanges = produce(evaluationTimeRanges, (draft) => {
|
||||||
|
draft[refId] = timeRange;
|
||||||
|
});
|
||||||
|
setEvaluationTimeRanges(newEvalTimeRanges);
|
||||||
|
},
|
||||||
|
[evaluationTimeRanges, setEvaluationTimeRanges]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isFederatedRule = isFederatedRuleGroup(rule.group);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loadingData ? (
|
||||||
|
'Loading...'
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isGrafanaRulerRule(rule.rulerRule) && !isFederatedRule && (
|
||||||
|
<GrafanaRuleQueryViewer
|
||||||
|
condition={rule.rulerRule.grafana_alert.condition}
|
||||||
|
queries={queries}
|
||||||
|
evalDataByQuery={data}
|
||||||
|
evalTimeRanges={evaluationTimeRanges}
|
||||||
|
onTimeRangeChange={onQueryTimeRangeChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isGrafanaRulerRule(rule.rulerRule) && !isFederatedRule && data && Object.keys(data).length > 0 && (
|
||||||
|
<div>
|
||||||
|
{queries.map((query) => {
|
||||||
|
return (
|
||||||
|
<QueryPreview
|
||||||
|
key={query.refId}
|
||||||
|
refId={query.refId}
|
||||||
|
model={query.model}
|
||||||
|
dataSource={Object.values(config.datasources).find((ds) => ds.uid === query.datasourceUid)}
|
||||||
|
queryData={data[query.refId]}
|
||||||
|
relativeTimeRange={query.relativeTimeRange}
|
||||||
|
evalTimeRange={evaluationTimeRanges[query.refId]}
|
||||||
|
onEvalTimeRangeChange={(timeRange) => onQueryTimeRangeChange(query.refId, timeRange)}
|
||||||
|
isAlertCondition={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isFederatedRule && !allDataSourcesAvailable && (
|
||||||
|
<Alert title="Query not available" severity="warning">
|
||||||
|
Cannot display the query preview. Some of the data sources used in the queries are not available.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isLoading(data?: Record<string, PanelData>): boolean {
|
||||||
|
if (!data) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!Object.values(data).find((d) => d.state === LoadingState.Loading);
|
||||||
|
}
|
||||||
|
|
||||||
export { QueryResults };
|
export { QueryResults };
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ConfirmModal } from '@grafana/ui';
|
||||||
|
import { dispatch } from 'app/store/store';
|
||||||
|
import { CombinedRule } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { deleteRuleAction } from '../../../state/actions';
|
||||||
|
import { getRulesSourceName } from '../../../utils/datasource';
|
||||||
|
import { fromRulerRule } from '../../../utils/rule-id';
|
||||||
|
|
||||||
|
type DeleteModalHook = [JSX.Element, (rule: CombinedRule) => void, () => void];
|
||||||
|
|
||||||
|
export const useDeleteModal = (): DeleteModalHook => {
|
||||||
|
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule | undefined>();
|
||||||
|
|
||||||
|
const dismissModal = useCallback(() => {
|
||||||
|
setRuleToDelete(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showModal = useCallback((rule: CombinedRule) => {
|
||||||
|
setRuleToDelete(rule);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteRule = useCallback(
|
||||||
|
(ruleToDelete?: CombinedRule) => {
|
||||||
|
if (ruleToDelete && ruleToDelete.rulerRule) {
|
||||||
|
const identifier = fromRulerRule(
|
||||||
|
getRulesSourceName(ruleToDelete.namespace.rulesSource),
|
||||||
|
ruleToDelete.namespace.name,
|
||||||
|
ruleToDelete.group.name,
|
||||||
|
ruleToDelete.rulerRule
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(deleteRuleAction(identifier, { navigateTo: '/alerting/list' }));
|
||||||
|
dismissModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dismissModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const modal = useMemo(
|
||||||
|
() => (
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={Boolean(ruleToDelete)}
|
||||||
|
title="Delete rule"
|
||||||
|
body="Deleting this rule will permanently remove it from your alert rule list. Are you sure you want to delete this rule?"
|
||||||
|
confirmText="Yes, delete"
|
||||||
|
icon="exclamation-triangle"
|
||||||
|
onConfirm={() => deleteRule(ruleToDelete)}
|
||||||
|
onDismiss={dismissModal}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[deleteRule, dismissModal, ruleToDelete]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [modal, showModal, dismissModal];
|
||||||
|
};
|
@ -1,97 +1,147 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import { isEmpty, truncate } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { Alert, Button, Icon, LoadingPlaceholder, Tab, TabContent, TabsBar, Text, Stack } from '@grafana/ui';
|
import { AppEvents, NavModelItem, UrlQueryValue } from '@grafana/data';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { Alert, Button, Dropdown, LinkButton, Menu, Stack, TabContent, Text, TextLink } from '@grafana/ui';
|
||||||
import { GrafanaAlertState } from 'app/types/unified-alerting-dto';
|
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';
|
||||||
|
|
||||||
import { useRuleViewerPageTitle } from '../../../hooks/alert-details/useRuleViewerPageTitle';
|
import { defaultPageNav } from '../../../RuleViewer';
|
||||||
import { useCombinedRule } from '../../../hooks/useCombinedRule';
|
import { AlertRuleAction, useAlertRuleAbility } from '../../../hooks/useAbilities';
|
||||||
|
import { Annotation } from '../../../utils/constants';
|
||||||
|
import {
|
||||||
|
createShareLink,
|
||||||
|
isLocalDevEnv,
|
||||||
|
isOpenSourceEdition,
|
||||||
|
makeDashboardLink,
|
||||||
|
makePanelLink,
|
||||||
|
makeRuleBasedSilenceLink,
|
||||||
|
} from '../../../utils/misc';
|
||||||
import * as ruleId from '../../../utils/rule-id';
|
import * as ruleId from '../../../utils/rule-id';
|
||||||
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../../utils/rules';
|
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../../utils/rules';
|
||||||
|
import { createUrl } from '../../../utils/url';
|
||||||
|
import { AlertLabels } from '../../AlertLabels';
|
||||||
import { AlertStateDot } from '../../AlertStateDot';
|
import { AlertStateDot } from '../../AlertStateDot';
|
||||||
|
import { AlertingPageWrapper } from '../../AlertingPageWrapper';
|
||||||
|
import MoreButton from '../../MoreButton';
|
||||||
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
|
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
|
||||||
import { Spacer } from '../../Spacer';
|
import { DeclareIncidentMenuItem } from '../../bridges/DeclareIncidentButton';
|
||||||
|
import { Details } from '../tabs/Details';
|
||||||
import { History } from '../tabs/History';
|
import { History } from '../tabs/History';
|
||||||
import { InstancesList } from '../tabs/Instances';
|
import { InstancesList } from '../tabs/Instances';
|
||||||
import { QueryResults } from '../tabs/Query';
|
import { QueryResults } from '../tabs/Query';
|
||||||
import { Routing } from '../tabs/Routing';
|
import { Routing } from '../tabs/Routing';
|
||||||
|
|
||||||
type RuleViewerProps = GrafanaRouteComponentProps<{
|
import { useDeleteModal } from './DeleteModal';
|
||||||
id: string;
|
|
||||||
sourceName: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
enum Tabs {
|
type RuleViewerProps = {
|
||||||
Instances,
|
rule: CombinedRule;
|
||||||
Query,
|
identifier: RuleIdentifier;
|
||||||
Routing,
|
};
|
||||||
History,
|
|
||||||
|
enum ActiveTab {
|
||||||
|
Query = 'query',
|
||||||
|
Instances = 'instances',
|
||||||
|
History = 'history',
|
||||||
|
Routing = 'routing',
|
||||||
|
Details = 'details',
|
||||||
}
|
}
|
||||||
|
|
||||||
// @TODO
|
const RuleViewer = ({ rule, identifier }: RuleViewerProps) => {
|
||||||
// hook up tabs to query params or path segment
|
const { pageNav, activeTab } = usePageNav(rule);
|
||||||
// figure out why we needed <AlertingPageWrapper>
|
const [deleteModal, showDeleteModal] = useDeleteModal();
|
||||||
// add provisioning and federation stuff back in
|
|
||||||
const RuleViewer = ({ match }: RuleViewerProps) => {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(Tabs.Instances);
|
|
||||||
|
|
||||||
const id = ruleId.getRuleIdFromPathname(match.params);
|
const [editSupported, editAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update);
|
||||||
const identifier = useMemo(() => {
|
const canEdit = editSupported && editAllowed;
|
||||||
if (!id) {
|
|
||||||
throw new Error('Rule ID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
return ruleId.parse(id, true);
|
const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
|
||||||
}, [id]);
|
const canDelete = deleteSupported && deleteAllowed;
|
||||||
|
|
||||||
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
|
const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
|
||||||
|
const canDuplicate = duplicateSupported && duplicateAllowed;
|
||||||
|
|
||||||
// we're setting the document title and the breadcrumb manually
|
const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
|
||||||
useRuleViewerPageTitle(rule);
|
const canSilence = silenceSupported && silenceAllowed;
|
||||||
|
|
||||||
if (loading) {
|
const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
|
||||||
return <LoadingPlaceholder text={'Loading...'} />;
|
const canExport = exportSupported && exportAllowed;
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return String(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule) {
|
|
||||||
const summary = rule.annotations['summary'];
|
|
||||||
const promRule = rule.promRule;
|
const promRule = rule.promRule;
|
||||||
|
|
||||||
const isAlertType = isAlertingRule(promRule);
|
const isAlertType = isAlertingRule(promRule);
|
||||||
const numberOfInstance = isAlertType ? promRule.alerts?.length : undefined;
|
|
||||||
|
|
||||||
const isFederatedRule = isFederatedRuleGroup(rule.group);
|
const isFederatedRule = isFederatedRuleGroup(rule.group);
|
||||||
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
|
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 (
|
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 && (
|
||||||
<>
|
<>
|
||||||
<Stack direction="column" gap={3}>
|
<Menu.Divider />
|
||||||
{/* breadcrumb and actions */}
|
<Menu.Item label="Delete" icon="trash-alt" destructive onClick={() => showDeleteModal(rule)} />
|
||||||
<Stack>
|
</>
|
||||||
<BreadCrumb folder={rule.namespace.name} evaluationGroup={rule.group.name} />
|
)}
|
||||||
<Spacer />
|
</Menu>
|
||||||
<Stack gap={1}>
|
}
|
||||||
<Button variant="secondary" icon="pen">
|
>
|
||||||
Edit
|
<MoreButton size="md" />
|
||||||
</Button>
|
</Dropdown>,
|
||||||
<Button variant="secondary">
|
]}
|
||||||
<Stack alignItems="center" gap={1}>
|
info={createMetadata(rule)}
|
||||||
More <Icon name="angle-down" />
|
>
|
||||||
</Stack>
|
<Stack direction="column" gap={2}>
|
||||||
</Button>
|
{/* actions */}
|
||||||
</Stack>
|
<Stack direction="column" gap={2}>
|
||||||
</Stack>
|
|
||||||
{/* header */}
|
|
||||||
<Stack direction="column" gap={1}>
|
|
||||||
<Stack alignItems="center">
|
|
||||||
<Title name={rule.name} state={GrafanaAlertState.Alerting} />
|
|
||||||
</Stack>
|
|
||||||
{summary && <Summary text={summary} />}
|
|
||||||
</Stack>
|
|
||||||
{/* alerts and notifications and stuff */}
|
{/* alerts and notifications and stuff */}
|
||||||
{isFederatedRule && (
|
{isFederatedRule && (
|
||||||
<Alert severity="info" title="This rule is part of a federated rule group.">
|
<Alert severity="info" title="This rule is part of a federated rule group.">
|
||||||
@ -107,74 +157,237 @@ const RuleViewer = ({ match }: RuleViewerProps) => {
|
|||||||
)}
|
)}
|
||||||
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.AlertRule} />}
|
{isProvisioned && <ProvisioningAlert resource={ProvisionedResource.AlertRule} />}
|
||||||
{/* tabs and tab content */}
|
{/* tabs and tab content */}
|
||||||
<TabsBar>
|
|
||||||
<Tab label="Instances" active counter={numberOfInstance} onChangeTab={() => setActiveTab(Tabs.Instances)} />
|
|
||||||
<Tab label="Query" onChangeTab={() => setActiveTab(Tabs.Query)} />
|
|
||||||
<Tab label="Routing" onChangeTab={() => setActiveTab(Tabs.Routing)} />
|
|
||||||
<Tab label="History" onChangeTab={() => setActiveTab(Tabs.History)} />
|
|
||||||
</TabsBar>
|
|
||||||
<TabContent>
|
<TabContent>
|
||||||
{activeTab === Tabs.Instances && <InstancesList />}
|
{activeTab === ActiveTab.Query && <QueryResults rule={rule} />}
|
||||||
{activeTab === Tabs.Query && <QueryResults />}
|
{activeTab === ActiveTab.Instances && <InstancesList rule={rule} />}
|
||||||
{activeTab === Tabs.Routing && <Routing />}
|
{activeTab === ActiveTab.History && isGrafanaRulerRule(rule.rulerRule) && <History rule={rule.rulerRule} />}
|
||||||
{activeTab === Tabs.History && <History />}
|
{activeTab === ActiveTab.Routing && <Routing />}
|
||||||
|
{activeTab === ActiveTab.Details && <Details rule={rule} />}
|
||||||
</TabContent>
|
</TabContent>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</Stack>
|
||||||
|
{deleteModal}
|
||||||
|
</AlertingPageWrapper>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface BreadcrumbProps {
|
interface EditButtonProps {
|
||||||
folder: string;
|
identifier: RuleIdentifier;
|
||||||
evaluationGroup: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BreadCrumb = ({ folder, evaluationGroup }: BreadcrumbProps) => (
|
export const EditButton = ({ identifier }: EditButtonProps) => {
|
||||||
<Stack alignItems="center" gap={0.5}>
|
const returnTo = location.pathname + location.search;
|
||||||
<Text color="secondary">
|
const ruleIdentifier = ruleId.stringifyIdentifier(identifier);
|
||||||
<Icon name="folder" />
|
const editURL = createUrl(`/alerting/${encodeURIComponent(ruleIdentifier)}/edit`, { returnTo });
|
||||||
</Text>
|
|
||||||
<Text variant="body" color="primary">
|
return (
|
||||||
{folder}
|
<LinkButton variant="secondary" icon="pen" href={editURL}>
|
||||||
</Text>
|
Edit
|
||||||
<Text variant="body" color="secondary">
|
</LinkButton>
|
||||||
<Icon name="angle-right" />
|
);
|
||||||
</Text>
|
};
|
||||||
<Text variant="body" color="primary">
|
|
||||||
{evaluationGroup}
|
const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
|
||||||
</Text>
|
const { labels, annotations, group } = rule;
|
||||||
</Stack>
|
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>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
};
|
||||||
|
|
||||||
interface TitleProps {
|
interface TitleProps {
|
||||||
name: string;
|
name: string;
|
||||||
state: GrafanaAlertState;
|
// recording rules don't have a state
|
||||||
|
state?: PromAlertingRuleState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Title = ({ name, state }: TitleProps) => (
|
export const Title = ({ name, state }: TitleProps) => (
|
||||||
<header>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, maxWidth: '100%' }}>
|
||||||
<Stack alignItems={'center'} gap={1}>
|
<LinkButton variant="secondary" icon="angle-left" href="/alerting/list" />
|
||||||
<AlertStateDot size="md" state={state} />
|
<Text element="h1" truncate>
|
||||||
{/* <Button variant="secondary" fill="outline" icon="angle-left" /> */}
|
|
||||||
<Text element="h1" variant="h2" weight="bold">
|
|
||||||
{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
{/* <Badge color="red" text={state} icon="exclamation-circle" /> */}
|
{/* recording rules won't have a state */}
|
||||||
</Stack>
|
{state && <StateBadge state={state} />}
|
||||||
</header>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface SummaryProps {
|
interface StateBadgeProps {
|
||||||
text: string;
|
state: PromAlertingRuleState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Summary = ({ text }: SummaryProps) => (
|
// TODO move to separate component
|
||||||
<Text variant="body" color="secondary">
|
const StateBadge = ({ state }: StateBadgeProps) => {
|
||||||
{text}
|
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}>
|
||||||
|
<AlertStateDot size="md" state={state} />
|
||||||
|
<Text variant="bodySmall" color={textColor}>
|
||||||
|
{stateLabel}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function useActiveTab(): [ActiveTab, (tab: ActiveTab) => void] {
|
||||||
|
const [queryParams, setQueryParams] = useQueryParams();
|
||||||
|
const tabFromQuery = queryParams['tab'];
|
||||||
|
|
||||||
|
const activeTab = isValidTab(tabFromQuery) ? tabFromQuery : ActiveTab.Query;
|
||||||
|
|
||||||
|
const setActiveTab = (tab: ActiveTab) => {
|
||||||
|
setQueryParams({ tab });
|
||||||
|
};
|
||||||
|
|
||||||
|
return [activeTab, setActiveTab];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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: {
|
||||||
|
text: rule.group.name,
|
||||||
|
url: createListFilterLink([
|
||||||
|
['namespace', rule.namespace.name],
|
||||||
|
['group', rule.group.name],
|
||||||
|
]),
|
||||||
|
parentItem: {
|
||||||
|
text: rule.namespace.name,
|
||||||
|
url: createListFilterLink([['namespace', rule.namespace.name]]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageNav,
|
||||||
|
activeTab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default RuleViewer;
|
export default RuleViewer;
|
||||||
|
@ -53,7 +53,9 @@ export const RuleDetails = ({ rule }: Props) => {
|
|||||||
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
|
<RuleDetailsDataSources rulesSource={rulesSource} rule={rule} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<DetailsField label="Matching instances" horizontal={true}>
|
||||||
<RuleDetailsMatchingInstances rule={rule} itemsDisplayLimit={INSTANCES_DISPLAY_LIMIT} />
|
<RuleDetailsMatchingInstances rule={rule} itemsDisplayLimit={INSTANCES_DISPLAY_LIMIT} />
|
||||||
|
</DetailsField>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -37,7 +37,7 @@ import {
|
|||||||
import * as ruleId from '../../utils/rule-id';
|
import * as ruleId from '../../utils/rule-id';
|
||||||
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { createUrl } from '../../utils/url';
|
import { createUrl } from '../../utils/url';
|
||||||
import { DeclareIncident } from '../bridges/DeclareIncidentButton';
|
import { DeclareIncidentButton } from '../bridges/DeclareIncidentButton';
|
||||||
|
|
||||||
import { RedirectToCloneRule } from './CloneRule';
|
import { RedirectToCloneRule } from './CloneRule';
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
|
|||||||
if (isFiringRule && shouldShowDeclareIncidentButton()) {
|
if (isFiringRule && shouldShowDeclareIncidentButton()) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Fragment key="declare-incident">
|
<Fragment key="declare-incident">
|
||||||
<DeclareIncident title={rule.name} url={buildShareUrl()} />
|
<DeclareIncidentButton title={rule.name} url={buildShareUrl()} />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -318,7 +318,8 @@ function shouldShowDeclareIncidentButton() {
|
|||||||
|
|
||||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
padding: ${theme.spacing(2)} 0;
|
padding: 0 0 ${theme.spacing(2)} 0;
|
||||||
|
gap: ${theme.spacing(1)};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -18,7 +18,6 @@ import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
|
|||||||
|
|
||||||
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
import { isAlertingRule } from '../../utils/rules';
|
import { isAlertingRule } from '../../utils/rules';
|
||||||
import { DetailsField } from '../DetailsField';
|
|
||||||
|
|
||||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||||
import { getComponentsFromStats } from './RuleStats';
|
import { getComponentsFromStats } from './RuleStats';
|
||||||
@ -83,7 +82,14 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
|
|
||||||
// Count All By State is used only when filtering is enabled and we have access to all instances
|
// Count All By State is used only when filtering is enabled and we have access to all instances
|
||||||
const countAllByState = countBy(promRule.alerts, (alert) => mapStateWithReasonToBaseState(alert.state));
|
const countAllByState = countBy(promRule.alerts, (alert) => mapStateWithReasonToBaseState(alert.state));
|
||||||
const totalInstancesCount = sum(Object.values(instanceTotals));
|
|
||||||
|
// error state is not a separate state
|
||||||
|
const totalInstancesCount = sum([
|
||||||
|
instanceTotals.alerting,
|
||||||
|
instanceTotals.inactive,
|
||||||
|
instanceTotals.pending,
|
||||||
|
instanceTotals.nodata,
|
||||||
|
]);
|
||||||
const hiddenInstancesCount = totalInstancesCount - visibleInstances.length;
|
const hiddenInstancesCount = totalInstancesCount - visibleInstances.length;
|
||||||
|
|
||||||
const stats: ShowMoreStats = {
|
const stats: ShowMoreStats = {
|
||||||
@ -104,7 +110,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DetailsField label="Matching instances" horizontal={true}>
|
<>
|
||||||
{enableFiltering && (
|
{enableFiltering && (
|
||||||
<div className={cx(styles.flexRow, styles.spaceBetween)}>
|
<div className={cx(styles.flexRow, styles.spaceBetween)}>
|
||||||
<div className={styles.flexRow}>
|
<div className={styles.flexRow}>
|
||||||
@ -126,7 +132,7 @@ export function RuleDetailsMatchingInstances(props: Props): JSX.Element | null {
|
|||||||
)}
|
)}
|
||||||
{!enableFiltering && <div className={styles.stats}>{statsComponents}</div>}
|
{!enableFiltering && <div className={styles.stats}>{statsComponents}</div>}
|
||||||
<AlertInstancesTable rule={rule} instances={visibleInstances} pagination={pagination} footerRow={footerRow} />
|
<AlertInstancesTable rule={rule} instances={visibleInstances} pagination={pagination} footerRow={footerRow} />
|
||||||
</DetailsField>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { isEmpty, sortBy, take, uniq } from 'lodash';
|
import { fromPairs, isEmpty, sortBy, take, uniq } from 'lodash';
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data';
|
import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||||
import { Alert, Button, Field, Icon, Input, Label, TagList, Tooltip, useStyles2, Stack } from '@grafana/ui';
|
import { Alert, Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui';
|
||||||
|
|
||||||
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
||||||
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
import { combineMatcherStrings } from '../../../utils/alertmanager';
|
||||||
|
import { AlertLabels } from '../../AlertLabels';
|
||||||
import { HoverCard } from '../../HoverCard';
|
import { HoverCard } from '../../HoverCard';
|
||||||
|
|
||||||
import { LogRecordViewerByTimestamp } from './LogRecordViewer';
|
import { LogRecordViewerByTimestamp } from './LogRecordViewer';
|
||||||
@ -119,8 +120,8 @@ const LokiStateHistory = ({ ruleUID }: Props) => {
|
|||||||
<Tooltip content="Common labels are the ones attached to all of the alert instances">
|
<Tooltip content="Common labels are the ones attached to all of the alert instances">
|
||||||
<Icon name="info-circle" />
|
<Icon name="info-circle" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<AlertLabels labels={fromPairs(commonLabels)} size="sm" />
|
||||||
</Stack>
|
</Stack>
|
||||||
<TagList tags={commonLabels.map((label) => label.join('='))} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isEmpty(frameSubset) ? (
|
{isEmpty(frameSubset) ? (
|
||||||
|
@ -67,7 +67,9 @@ const StateHistory = ({ alertId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div key={groupKey}>
|
<div key={groupKey}>
|
||||||
<header className={styles.tableGroupKey}>
|
<header className={styles.tableGroupKey}>
|
||||||
<code>{groupKey}</code>
|
<code className={styles.goupKeyText} aria-label={groupKey}>
|
||||||
|
{groupKey}
|
||||||
|
</code>
|
||||||
</header>
|
</header>
|
||||||
<DynamicTable cols={columns} items={tableItems} pagination={{ itemsPerPage: 25 }} />
|
<DynamicTable cols={columns} items={tableItems} pagination={{ itemsPerPage: 25 }} />
|
||||||
</div>
|
</div>
|
||||||
@ -198,6 +200,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin-top: ${theme.spacing(2)};
|
margin-top: ${theme.spacing(2)};
|
||||||
margin-bottom: ${theme.spacing(2)};
|
margin-bottom: ${theme.spacing(2)};
|
||||||
`,
|
`,
|
||||||
|
goupKeyText: css({
|
||||||
|
overflowX: 'auto',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'block',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default StateHistory;
|
export default StateHistory;
|
||||||
|
@ -9,7 +9,7 @@ import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
|||||||
const AnnotationsStateHistory = lazy(() => import('../components/rules/state-history/StateHistory'));
|
const AnnotationsStateHistory = lazy(() => import('../components/rules/state-history/StateHistory'));
|
||||||
const LokiStateHistory = lazy(() => import('../components/rules/state-history/LokiStateHistory'));
|
const LokiStateHistory = lazy(() => import('../components/rules/state-history/LokiStateHistory'));
|
||||||
|
|
||||||
enum StateHistoryImplementation {
|
export enum StateHistoryImplementation {
|
||||||
Loki = 'loki',
|
Loki = 'loki',
|
||||||
Annotations = 'annotations',
|
Annotations = 'annotations',
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user