mirror of https://github.com/grafana/grafana
Alerting: added possibility to preview grafana managed alert rules. (#34600)
* starting to add eval logic. * wip * first version of test rule. * reverted file. * add info colum to result to show error or (with CC evalmatches) * fix labels in evalmatch * fix be test * refactored using observables. * moved widht/height div to outside panel rendere. * adding docs api level. * adding container styles to error div. * increasing size of preview. Co-authored-by: kyle <kyle@grafana.com>pull/34674/head^2
parent
42f33630c7
commit
e19b3df1a9
@ -0,0 +1,17 @@ |
|||||||
|
import { merge, Observable, timer } from 'rxjs'; |
||||||
|
import { mapTo, takeUntil } from 'rxjs/operators'; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
export type WithLoadingIndicatorOptions<T> = { |
||||||
|
whileLoading: T; |
||||||
|
source: Observable<T>; |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
export function withLoadingIndicator<T>({ whileLoading, source }: WithLoadingIndicatorOptions<T>): Observable<T> { |
||||||
|
return merge(timer(200).pipe(mapTo(whileLoading), takeUntil(source)), source); |
||||||
|
} |
||||||
@ -0,0 +1,83 @@ |
|||||||
|
import { |
||||||
|
dataFrameFromJSON, |
||||||
|
DataFrameJSON, |
||||||
|
getDefaultTimeRange, |
||||||
|
LoadingState, |
||||||
|
PanelData, |
||||||
|
withLoadingIndicator, |
||||||
|
} from '@grafana/data'; |
||||||
|
import { getBackendSrv, toDataQueryError } from '@grafana/runtime'; |
||||||
|
import { Observable, of } from 'rxjs'; |
||||||
|
import { catchError, map, share } from 'rxjs/operators'; |
||||||
|
import { |
||||||
|
CloudPreviewRuleRequest, |
||||||
|
GrafanaPreviewRuleRequest, |
||||||
|
isCloudPreviewRequest, |
||||||
|
isGrafanaPreviewRequest, |
||||||
|
PreviewRuleRequest, |
||||||
|
PreviewRuleResponse, |
||||||
|
} from '../types/preview'; |
||||||
|
import { RuleFormType } from '../types/rule-form'; |
||||||
|
|
||||||
|
export function previewAlertRule(request: PreviewRuleRequest): Observable<PreviewRuleResponse> { |
||||||
|
if (isCloudPreviewRequest(request)) { |
||||||
|
return previewCloudAlertRule(request); |
||||||
|
} |
||||||
|
|
||||||
|
if (isGrafanaPreviewRequest(request)) { |
||||||
|
return previewGrafanaAlertRule(request); |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error('unsupported preview rule request'); |
||||||
|
} |
||||||
|
|
||||||
|
type GrafanaPreviewRuleResponse = { |
||||||
|
instances: DataFrameJSON[]; |
||||||
|
}; |
||||||
|
|
||||||
|
function previewGrafanaAlertRule(request: GrafanaPreviewRuleRequest): Observable<PreviewRuleResponse> { |
||||||
|
const type = RuleFormType.grafana; |
||||||
|
|
||||||
|
return withLoadingIndicator({ |
||||||
|
whileLoading: createResponse(type), |
||||||
|
source: getBackendSrv() |
||||||
|
.fetch<GrafanaPreviewRuleResponse>({ |
||||||
|
method: 'POST', |
||||||
|
url: `/api/v1/rule/test/grafana`, |
||||||
|
data: request, |
||||||
|
}) |
||||||
|
.pipe( |
||||||
|
map(({ data }) => { |
||||||
|
return createResponse(type, { |
||||||
|
state: LoadingState.Done, |
||||||
|
series: data.instances.map(dataFrameFromJSON), |
||||||
|
}); |
||||||
|
}), |
||||||
|
catchError((error: Error) => { |
||||||
|
return of( |
||||||
|
createResponse(type, { |
||||||
|
state: LoadingState.Error, |
||||||
|
error: toDataQueryError(error), |
||||||
|
}) |
||||||
|
); |
||||||
|
}), |
||||||
|
share() |
||||||
|
), |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function createResponse(ruleType: RuleFormType, data: Partial<PanelData> = {}): PreviewRuleResponse { |
||||||
|
return { |
||||||
|
ruleType, |
||||||
|
data: { |
||||||
|
state: LoadingState.Loading, |
||||||
|
series: [], |
||||||
|
timeRange: getDefaultTimeRange(), |
||||||
|
...data, |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function previewCloudAlertRule(request: CloudPreviewRuleRequest): Observable<PreviewRuleResponse> { |
||||||
|
throw new Error('preview for cloud alerting rules is not implemented'); |
||||||
|
} |
||||||
@ -0,0 +1,99 @@ |
|||||||
|
import React, { useCallback, useState } from 'react'; |
||||||
|
import { css } from '@emotion/css'; |
||||||
|
import { useFormContext } from 'react-hook-form'; |
||||||
|
import { takeWhile } from 'rxjs/operators'; |
||||||
|
import { useMountedState } from 'react-use'; |
||||||
|
import { Button, HorizontalGroup, useStyles2 } from '@grafana/ui'; |
||||||
|
import { dateTimeFormatISO, GrafanaTheme2, LoadingState } from '@grafana/data'; |
||||||
|
import { RuleFormType } from '../../types/rule-form'; |
||||||
|
import { PreviewRuleRequest, PreviewRuleResponse } from '../../types/preview'; |
||||||
|
import { previewAlertRule } from '../../api/preview'; |
||||||
|
import { PreviewRuleResult } from './PreviewRuleResult'; |
||||||
|
|
||||||
|
const fields: string[] = ['type', 'dataSourceName', 'condition', 'queries', 'expression']; |
||||||
|
|
||||||
|
export function PreviewRule(): React.ReactElement | null { |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
const [preview, onPreview] = usePreview(); |
||||||
|
const { getValues } = useFormContext(); |
||||||
|
const [type] = getValues(fields); |
||||||
|
|
||||||
|
if (type === RuleFormType.cloud) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.container}> |
||||||
|
<HorizontalGroup> |
||||||
|
<Button type="button" variant="primary" onClick={onPreview}> |
||||||
|
Preview your alert |
||||||
|
</Button> |
||||||
|
</HorizontalGroup> |
||||||
|
<PreviewRuleResult preview={preview} /> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function usePreview(): [PreviewRuleResponse | undefined, () => void] { |
||||||
|
const [preview, setPreview] = useState<PreviewRuleResponse | undefined>(); |
||||||
|
const { getValues } = useFormContext(); |
||||||
|
const isMounted = useMountedState(); |
||||||
|
|
||||||
|
const onPreview = useCallback(() => { |
||||||
|
const values = getValues(fields); |
||||||
|
const request = createPreviewRequest(values); |
||||||
|
|
||||||
|
previewAlertRule(request) |
||||||
|
.pipe(takeWhile((response) => !isCompleted(response), true)) |
||||||
|
.subscribe((response) => { |
||||||
|
if (!isMounted()) { |
||||||
|
return; |
||||||
|
} |
||||||
|
setPreview(response); |
||||||
|
}); |
||||||
|
}, [getValues, isMounted]); |
||||||
|
|
||||||
|
return [preview, onPreview]; |
||||||
|
} |
||||||
|
|
||||||
|
function createPreviewRequest(values: any[]): PreviewRuleRequest { |
||||||
|
const [type, dataSourceName, condition, queries, expression] = values; |
||||||
|
|
||||||
|
switch (type) { |
||||||
|
case RuleFormType.cloud: |
||||||
|
return { |
||||||
|
dataSourceName, |
||||||
|
expr: expression, |
||||||
|
}; |
||||||
|
|
||||||
|
case RuleFormType.grafana: |
||||||
|
return { |
||||||
|
grafana_condition: { |
||||||
|
condition, |
||||||
|
data: queries, |
||||||
|
now: dateTimeFormatISO(Date.now()), |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
default: |
||||||
|
throw new Error(`Alert type ${type} not supported by preview.`); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function isCompleted(response: PreviewRuleResponse): boolean { |
||||||
|
switch (response.data.state) { |
||||||
|
case LoadingState.Done: |
||||||
|
case LoadingState.Error: |
||||||
|
return true; |
||||||
|
default: |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) { |
||||||
|
return { |
||||||
|
container: css` |
||||||
|
margin-top: ${theme.spacing(2)}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { css } from '@emotion/css'; |
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer'; |
||||||
|
import { useStyles2 } from '@grafana/ui'; |
||||||
|
import { PanelRenderer } from '@grafana/runtime'; |
||||||
|
import { GrafanaTheme2, LoadingState } from '@grafana/data'; |
||||||
|
import { PreviewRuleResponse } from '../../types/preview'; |
||||||
|
import { RuleFormType } from '../../types/rule-form'; |
||||||
|
|
||||||
|
type Props = { |
||||||
|
preview: PreviewRuleResponse | undefined; |
||||||
|
}; |
||||||
|
|
||||||
|
export function PreviewRuleResult(props: Props): React.ReactElement | null { |
||||||
|
const { preview } = props; |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
if (!preview) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
const { data, ruleType } = preview; |
||||||
|
|
||||||
|
if (data.state === LoadingState.Loading) { |
||||||
|
return ( |
||||||
|
<div className={styles.container}> |
||||||
|
<span>Loading preview...</span> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (data.state === LoadingState.Error) { |
||||||
|
return <div className={styles.container}>{data.error ?? 'Failed to preview alert rule'}</div>; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.container}> |
||||||
|
<span> |
||||||
|
Preview based on the result of running the query, for this moment.{' '} |
||||||
|
{ruleType === RuleFormType.grafana ? 'Configuration for `no data` and `error handling` is not applied.' : null} |
||||||
|
</span> |
||||||
|
<div className={styles.table}> |
||||||
|
<AutoSizer> |
||||||
|
{({ width, height }) => ( |
||||||
|
<div style={{ width: `${width}px`, height: `${height}px` }}> |
||||||
|
<PanelRenderer title="" width={width} height={height} pluginId="table" data={data} /> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</AutoSizer> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) { |
||||||
|
return { |
||||||
|
container: css` |
||||||
|
margin: ${theme.spacing(2)} 0; |
||||||
|
`,
|
||||||
|
table: css` |
||||||
|
flex: 1 1 auto; |
||||||
|
height: 135px; |
||||||
|
margin-top: ${theme.spacing(2)}; |
||||||
|
border: 1px solid ${theme.colors.border.medium}; |
||||||
|
border-radius: ${theme.shape.borderRadius(1)}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
import { PanelData } from '@grafana/data'; |
||||||
|
import { GrafanaQuery } from 'app/types/unified-alerting-dto'; |
||||||
|
import { RuleFormType } from './rule-form'; |
||||||
|
|
||||||
|
export type PreviewRuleRequest = GrafanaPreviewRuleRequest | CloudPreviewRuleRequest; |
||||||
|
|
||||||
|
export type GrafanaPreviewRuleRequest = { |
||||||
|
grafana_condition: { |
||||||
|
condition: string; |
||||||
|
data: GrafanaQuery[]; |
||||||
|
now: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
export type CloudPreviewRuleRequest = { |
||||||
|
dataSourceName: string; |
||||||
|
expr: string; |
||||||
|
}; |
||||||
|
|
||||||
|
export type PreviewRuleResponse = { |
||||||
|
ruleType: RuleFormType; |
||||||
|
data: PanelData; |
||||||
|
}; |
||||||
|
|
||||||
|
export function isCloudPreviewRequest(request: PreviewRuleRequest): request is CloudPreviewRuleRequest { |
||||||
|
return 'expr' in request; |
||||||
|
} |
||||||
|
|
||||||
|
export function isGrafanaPreviewRequest(request: PreviewRuleRequest): request is GrafanaPreviewRuleRequest { |
||||||
|
return 'grafana_condition' in request; |
||||||
|
} |
||||||
Loading…
Reference in new issue