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