mirror of https://github.com/grafana/grafana.git
Alerting: Add alert preview to cloud rules editor (#54950)
This commit is contained in:
parent
e8a60c1988
commit
7114c51f9f
|
|
@ -12,52 +12,53 @@ import {
|
|||
import { getBackendSrv, toDataQueryError } from '@grafana/runtime';
|
||||
|
||||
import {
|
||||
CloudPreviewRuleRequest,
|
||||
GrafanaPreviewRuleRequest,
|
||||
isCloudPreviewRequest,
|
||||
isGrafanaPreviewRequest,
|
||||
PreviewRuleRequest,
|
||||
PreviewRuleResponse,
|
||||
} from '../types/preview';
|
||||
import { RuleFormType } from '../types/rule-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
export function previewAlertRule(request: PreviewRuleRequest): Observable<PreviewRuleResponse> {
|
||||
if (isCloudPreviewRequest(request)) {
|
||||
return previewCloudAlertRule(request);
|
||||
return fetchAlertRulePreview(request, request.dataSourceUid, RuleFormType.cloudAlerting);
|
||||
}
|
||||
|
||||
if (isGrafanaPreviewRequest(request)) {
|
||||
return previewGrafanaAlertRule(request);
|
||||
return fetchAlertRulePreview(request, GRAFANA_RULES_SOURCE_NAME, RuleFormType.grafana);
|
||||
}
|
||||
|
||||
throw new Error('unsupported preview rule request');
|
||||
}
|
||||
|
||||
type GrafanaPreviewRuleResponse = {
|
||||
type AlertRulePreviewResponse = {
|
||||
instances: DataFrameJSON[];
|
||||
};
|
||||
|
||||
function previewGrafanaAlertRule(request: GrafanaPreviewRuleRequest): Observable<PreviewRuleResponse> {
|
||||
const type = RuleFormType.grafana;
|
||||
|
||||
function fetchAlertRulePreview(
|
||||
request: PreviewRuleRequest,
|
||||
dataSourceUid: string,
|
||||
ruleType: RuleFormType
|
||||
): Observable<PreviewRuleResponse> {
|
||||
return withLoadingIndicator({
|
||||
whileLoading: createResponse(type),
|
||||
whileLoading: createResponse(ruleType),
|
||||
source: getBackendSrv()
|
||||
.fetch<GrafanaPreviewRuleResponse>({
|
||||
.fetch<AlertRulePreviewResponse>({
|
||||
method: 'POST',
|
||||
url: `/api/v1/rule/test/grafana`,
|
||||
url: `/api/v1/rule/test/${dataSourceUid}`,
|
||||
data: request,
|
||||
})
|
||||
.pipe(
|
||||
map(({ data }) => {
|
||||
return createResponse(type, {
|
||||
return createResponse(ruleType, {
|
||||
state: LoadingState.Done,
|
||||
series: data.instances.map(dataFrameFromJSON),
|
||||
});
|
||||
}),
|
||||
catchError((error: Error) => {
|
||||
return of(
|
||||
createResponse(type, {
|
||||
createResponse(ruleType, {
|
||||
state: LoadingState.Error,
|
||||
error: toDataQueryError(error),
|
||||
})
|
||||
|
|
@ -79,7 +80,3 @@ function createResponse(ruleType: RuleFormType, data: Partial<PanelData> = {}):
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
function previewCloudAlertRule(request: CloudPreviewRuleRequest): Observable<PreviewRuleResponse> {
|
||||
throw new Error('preview for cloud alerting rules is not implemented');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export function ExternalAMdataSourceCard({ alertmanager, inactive }: ExternalAMd
|
|||
<Card.Meta>{url}</Card.Meta>
|
||||
<Card.Actions>
|
||||
<LinkButton href={makeDataSourceLink(dataSource)} size="sm" variant="secondary">
|
||||
Go to datasouce
|
||||
Go to datasource
|
||||
</LinkButton>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data/src';
|
||||
import { Icon, TagList, Tooltip, useStyles2 } from '@grafana/ui/src';
|
||||
|
||||
import { labelsToTags } from '../../utils/labels';
|
||||
import { AlertStateTag } from '../rules/AlertStateTag';
|
||||
|
||||
import { mapDataFrameToAlertPreview } from './preview';
|
||||
|
||||
interface CloudAlertPreviewProps {
|
||||
preview: DataFrame;
|
||||
}
|
||||
|
||||
export function CloudAlertPreview({ preview }: CloudAlertPreviewProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const alertPreview = mapDataFrameToAlertPreview(preview);
|
||||
|
||||
return (
|
||||
<table className={styles.table}>
|
||||
<caption>
|
||||
<div>Alerts preview</div>
|
||||
<span>Preview based on the result of running the query for this moment.</span>
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<th>Labels</th>
|
||||
<th>Info</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{alertPreview.instances.map(({ state, info, labels }, index) => {
|
||||
const instanceTags = labelsToTags(labels);
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>{<AlertStateTag state={state} />}</td>
|
||||
<td>
|
||||
<TagList tags={instanceTags} className={styles.tagList} />
|
||||
</td>
|
||||
<td>
|
||||
{info && (
|
||||
<Tooltip content={info}>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
table: css`
|
||||
width: 100%;
|
||||
margin: ${theme.spacing(2, 0)};
|
||||
|
||||
caption {
|
||||
caption-side: top;
|
||||
color: ${theme.colors.text.primary};
|
||||
|
||||
& > span {
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
color: ${theme.colors.text.secondary};
|
||||
}
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: ${theme.spacing(1, 1)};
|
||||
}
|
||||
|
||||
td + td,
|
||||
th + th {
|
||||
padding-left: ${theme.spacing(3)};
|
||||
}
|
||||
|
||||
thead th {
|
||||
&:nth-child(1) {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
td:nth-child(3) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n + 1) {
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
}
|
||||
`,
|
||||
tagList: css`
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
});
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
import { css } from '@emotion/css';
|
||||
import { noop } from 'lodash';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { CoreApp, DataQuery } from '@grafana/data';
|
||||
import { CoreApp, DataQuery, GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { Alert, Button, useStyles2 } from '@grafana/ui';
|
||||
import { LokiQuery } from 'app/plugins/datasource/loki/types';
|
||||
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
|
||||
|
||||
import { CloudAlertPreview } from './CloudAlertPreview';
|
||||
import { usePreview } from './PreviewRule';
|
||||
|
||||
export interface ExpressionEditorProps {
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
|
|
@ -14,8 +19,10 @@ export interface ExpressionEditorProps {
|
|||
}
|
||||
|
||||
export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, dataSourceName }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { mapToValue, mapToQuery } = useQueryMappers(dataSourceName);
|
||||
const query = mapToQuery({ refId: 'A', hide: false }, value);
|
||||
const dataQuery = mapToQuery({ refId: 'A', hide: false }, value);
|
||||
|
||||
const {
|
||||
error,
|
||||
|
|
@ -32,6 +39,12 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, d
|
|||
[onChange, mapToValue]
|
||||
);
|
||||
|
||||
const [alertPreview, onPreview] = usePreview();
|
||||
|
||||
const onRunQueriesClick = async () => {
|
||||
onPreview();
|
||||
};
|
||||
|
||||
if (loading || dataSource?.name !== dataSourceName) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -41,20 +54,51 @@ export const ExpressionEditor: FC<ExpressionEditorProps> = ({ value, onChange, d
|
|||
return <div>Could not load query editor due to: {errorMessage}</div>;
|
||||
}
|
||||
|
||||
const previewLoaded = alertPreview?.data.state === LoadingState.Done;
|
||||
|
||||
const QueryEditor = dataSource?.components?.QueryEditor;
|
||||
|
||||
// The Preview endpoint returns the preview as a single-element array of data frames
|
||||
const previewDataFrame = alertPreview?.data?.series?.find((s) => s.name === 'evaluation results');
|
||||
// The preview API returns arrays with empty elements when there are no firing alerts
|
||||
const previewHasAlerts = previewDataFrame && previewDataFrame.fields.some((field) => field.values.length > 0);
|
||||
|
||||
return (
|
||||
<QueryEditor
|
||||
query={query}
|
||||
queries={[query]}
|
||||
app={CoreApp.CloudAlerting}
|
||||
onChange={onChangeQuery}
|
||||
onRunQuery={noop}
|
||||
datasource={dataSource}
|
||||
/>
|
||||
<>
|
||||
<QueryEditor
|
||||
query={dataQuery}
|
||||
queries={[dataQuery]}
|
||||
app={CoreApp.CloudAlerting}
|
||||
onChange={onChangeQuery}
|
||||
onRunQuery={noop}
|
||||
datasource={dataSource}
|
||||
/>
|
||||
|
||||
<div className={styles.preview}>
|
||||
<Button type="button" onClick={onRunQueriesClick} disabled={alertPreview?.data.state === LoadingState.Loading}>
|
||||
Preview alerts
|
||||
</Button>
|
||||
{previewLoaded && !previewHasAlerts && (
|
||||
<Alert title="Alerts preview" severity="info" className={styles.previewAlert}>
|
||||
There are no firing alerts for your query.
|
||||
</Alert>
|
||||
)}
|
||||
{previewHasAlerts && <CloudAlertPreview preview={previewDataFrame} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
preview: css`
|
||||
padding: ${theme.spacing(2, 0)};
|
||||
max-width: ${theme.breakpoints.values.xl}px;
|
||||
`,
|
||||
previewAlert: css`
|
||||
margin: ${theme.spacing(1, 0)};
|
||||
`,
|
||||
});
|
||||
|
||||
type QueryMappers<T extends DataQuery = DataQuery> = {
|
||||
mapToValue: (query: T) => string;
|
||||
mapToQuery: (existing: T, value: string | undefined) => T;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { useMountedState } from 'react-use';
|
|||
import { takeWhile } from 'rxjs/operators';
|
||||
|
||||
import { dateTimeFormatISO, GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { Alert, Button, HorizontalGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { previewAlertRule } from '../../api/preview';
|
||||
|
|
@ -48,7 +49,7 @@ export function PreviewRule(): React.ReactElement | null {
|
|||
);
|
||||
}
|
||||
|
||||
function usePreview(): [PreviewRuleResponse | undefined, () => void] {
|
||||
export function usePreview(): [PreviewRuleResponse | undefined, () => void] {
|
||||
const [preview, setPreview] = useState<PreviewRuleResponse | undefined>();
|
||||
const { getValues } = useFormContext<RuleFormValues>();
|
||||
const isMounted = useMountedState();
|
||||
|
|
@ -72,10 +73,15 @@ function usePreview(): [PreviewRuleResponse | undefined, () => void] {
|
|||
|
||||
function createPreviewRequest(values: any[]): PreviewRuleRequest {
|
||||
const [type, dataSourceName, condition, queries, expression] = values;
|
||||
const dsSettings = getDataSourceSrv().getInstanceSettings(dataSourceName);
|
||||
if (!dsSettings) {
|
||||
throw new Error(`Cannot find data source settings for ${dataSourceName}`);
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case RuleFormType.cloudAlerting:
|
||||
return {
|
||||
dataSourceUid: dsSettings.uid,
|
||||
dataSourceName,
|
||||
expr: expression,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
import { DataFrame, FieldType, MutableDataFrame } from '@grafana/data';
|
||||
|
||||
import { mapDataFrameToAlertPreview } from './preview';
|
||||
|
||||
describe('mapDataFrameToAlertPreview', () => {
|
||||
it('should convert data frame fields into set of labels, state and info', () => {
|
||||
const frame: DataFrame = new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'severity',
|
||||
type: FieldType.string,
|
||||
values: ['error', 'error', 'warning', 'warning'],
|
||||
},
|
||||
{
|
||||
name: 'node',
|
||||
type: FieldType.string,
|
||||
values: ['cpu-0', 'cpu-1', 'cpu-0', 'cpu-1'],
|
||||
},
|
||||
{
|
||||
name: 'State',
|
||||
type: FieldType.string,
|
||||
values: ['Alerting', 'Alerting', 'Alerting', 'Alerting'],
|
||||
},
|
||||
{
|
||||
name: 'Info',
|
||||
type: FieldType.string,
|
||||
values: ['value=0.34', 'value=0.2', 'value=0.1', 'value=0.66'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const alertPreview = mapDataFrameToAlertPreview(frame);
|
||||
|
||||
expect(alertPreview.instances).toHaveLength(4);
|
||||
expect(alertPreview.instances[0]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: 'value=0.34',
|
||||
labels: { severity: 'error', node: 'cpu-0' },
|
||||
});
|
||||
expect(alertPreview.instances[1]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: 'value=0.2',
|
||||
labels: { severity: 'error', node: 'cpu-1' },
|
||||
});
|
||||
expect(alertPreview.instances[2]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: 'value=0.1',
|
||||
labels: { severity: 'warning', node: 'cpu-0' },
|
||||
});
|
||||
expect(alertPreview.instances[3]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: 'value=0.66',
|
||||
labels: { severity: 'warning', node: 'cpu-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 0 instances if there is no State field', () => {
|
||||
const frame: DataFrame = new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'severity',
|
||||
type: FieldType.string,
|
||||
values: ['error', 'warning'],
|
||||
},
|
||||
{
|
||||
name: 'Info',
|
||||
type: FieldType.string,
|
||||
values: ['value=0.34', 'value=0.2'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const alertPreview = mapDataFrameToAlertPreview(frame);
|
||||
|
||||
expect(alertPreview.instances).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return instances with labels if there is no Info field', () => {
|
||||
const frame: DataFrame = new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'severity',
|
||||
type: FieldType.string,
|
||||
values: ['error', 'warning'],
|
||||
},
|
||||
{
|
||||
name: 'State',
|
||||
type: FieldType.string,
|
||||
values: ['Alerting', 'Alerting'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const alertPreview = mapDataFrameToAlertPreview(frame);
|
||||
|
||||
expect(alertPreview.instances).toHaveLength(2);
|
||||
expect(alertPreview.instances[0]).toEqual({
|
||||
state: 'Alerting',
|
||||
labels: { severity: 'error' },
|
||||
});
|
||||
expect(alertPreview.instances[1]).toEqual({
|
||||
state: 'Alerting',
|
||||
labels: { severity: 'warning' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should limit number of instances to number of State values', () => {
|
||||
const frame: DataFrame = new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'severity',
|
||||
type: FieldType.string,
|
||||
values: ['critical', 'error', 'warning', 'info'],
|
||||
},
|
||||
{
|
||||
name: 'State',
|
||||
type: FieldType.string,
|
||||
values: ['Alerting', 'Alerting'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const alertPreview = mapDataFrameToAlertPreview(frame);
|
||||
|
||||
expect(alertPreview.instances).toHaveLength(2);
|
||||
expect(alertPreview.instances[0]).toEqual({ state: 'Alerting', labels: { severity: 'critical' } });
|
||||
expect(alertPreview.instances[1]).toEqual({ state: 'Alerting', labels: { severity: 'error' } });
|
||||
});
|
||||
|
||||
// Just to be resistant to incomplete data in data frames
|
||||
it('should return instances with labels if number of fields values do not match', () => {
|
||||
const frame: DataFrame = new MutableDataFrame({
|
||||
fields: [
|
||||
{
|
||||
name: 'severity',
|
||||
type: FieldType.string,
|
||||
values: ['error', 'error', 'warning', 'warning'],
|
||||
},
|
||||
{
|
||||
name: 'node',
|
||||
type: FieldType.string,
|
||||
values: ['cpu-0', 'cpu-1', 'cpu-1'],
|
||||
},
|
||||
{
|
||||
name: 'State',
|
||||
type: FieldType.string,
|
||||
values: ['Alerting', 'Alerting', 'Alerting', 'Alerting'],
|
||||
},
|
||||
{
|
||||
name: 'Info',
|
||||
type: FieldType.string,
|
||||
values: ['value=0.34', 'value=0.2', 'value=0.66'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const alertPreview = mapDataFrameToAlertPreview(frame);
|
||||
|
||||
expect(alertPreview.instances).toHaveLength(4);
|
||||
expect(alertPreview.instances[0]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: 'value=0.34',
|
||||
labels: { severity: 'error', node: 'cpu-0' },
|
||||
});
|
||||
expect(alertPreview.instances[1]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: 'value=0.2',
|
||||
labels: { severity: 'error', node: 'cpu-1' },
|
||||
});
|
||||
expect(alertPreview.instances[2]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: 'value=0.66',
|
||||
labels: { severity: 'warning', node: 'cpu-1' },
|
||||
});
|
||||
expect(alertPreview.instances[3]).toEqual({
|
||||
state: 'Alerting',
|
||||
info: undefined,
|
||||
labels: { severity: 'warning', node: undefined },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { DataFrame } from '@grafana/data';
|
||||
|
||||
import { GrafanaAlertState, isGrafanaAlertState, Labels } from '../../../../../types/unified-alerting-dto';
|
||||
|
||||
interface AlertPreviewInstance {
|
||||
state: GrafanaAlertState;
|
||||
info?: string;
|
||||
labels: Labels;
|
||||
}
|
||||
|
||||
interface AlertPreview {
|
||||
instances: AlertPreviewInstance[];
|
||||
}
|
||||
|
||||
// Alerts previews come in a DataFrame format which is more suited for displaying time series data
|
||||
// In order to display a list of tags we need to transform DataFrame into set of labels
|
||||
export function mapDataFrameToAlertPreview({ fields }: DataFrame): AlertPreview {
|
||||
const labelFields = fields.filter((field) => !['State', 'Info'].includes(field.name));
|
||||
const stateFieldIndex = fields.findIndex((field) => field.name === 'State');
|
||||
const infoFieldIndex = fields.findIndex((field) => field.name === 'Info');
|
||||
|
||||
const labelIndexes = labelFields.map((labelField) => fields.indexOf(labelField));
|
||||
|
||||
const instanceStatusCount = fields[stateFieldIndex]?.values.length ?? 0;
|
||||
|
||||
const instances: AlertPreviewInstance[] = [];
|
||||
|
||||
for (let index = 0; index < instanceStatusCount; index++) {
|
||||
const labelValues = labelIndexes.map((labelIndex) => [
|
||||
fields[labelIndex].name,
|
||||
fields[labelIndex].values.get(index),
|
||||
]);
|
||||
const state = fields[stateFieldIndex]?.values?.get(index);
|
||||
const info = fields[infoFieldIndex]?.values?.get(index);
|
||||
|
||||
if (isGrafanaAlertState(state)) {
|
||||
instances.push({
|
||||
state: state,
|
||||
info: info,
|
||||
labels: Object.fromEntries(labelValues),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { instances };
|
||||
}
|
||||
|
|
@ -14,8 +14,7 @@ export const Query: FC = () => {
|
|||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const type = watch('type');
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
const [type, dataSourceName] = watch(['type', 'dataSourceName']);
|
||||
|
||||
const isGrafanaManagedType = type === RuleFormType.grafana;
|
||||
const isCloudAlertRuleType = type === RuleFormType.cloudAlerting;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { FC } from 'react';
|
||||
|
||||
import { AlertState } from '@grafana/data';
|
||||
import { GrafanaAlertStateWithReason, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
import { GrafanaAlertState, GrafanaAlertStateWithReason, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { alertStateToReadable, alertStateToState } from '../../utils/rules';
|
||||
import { StateTag } from '../StateTag';
|
||||
interface Props {
|
||||
state: PromAlertingRuleState | GrafanaAlertStateWithReason | AlertState;
|
||||
state: PromAlertingRuleState | GrafanaAlertState | GrafanaAlertStateWithReason | AlertState;
|
||||
}
|
||||
|
||||
export const AlertStateTag: FC<Props> = ({ state }) => (
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export type GrafanaPreviewRuleRequest = {
|
|||
};
|
||||
|
||||
export type CloudPreviewRuleRequest = {
|
||||
dataSourceUid: string;
|
||||
dataSourceName: string;
|
||||
expr: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
import { Labels } from '../../../../types/unified-alerting-dto';
|
||||
|
||||
export function labelsToTags(labels: Labels) {
|
||||
return Object.entries(labels)
|
||||
.map(([label, value]) => `${label}=${value}`)
|
||||
.sort();
|
||||
}
|
||||
|
|
@ -23,23 +23,25 @@ export function alertRuleToQueries(combinedRule: CombinedRule | undefined | null
|
|||
if (isCloudRulesSource(rulesSource)) {
|
||||
const model = cloudAlertRuleToModel(rulesSource, combinedRule);
|
||||
|
||||
return [
|
||||
{
|
||||
refId: model.refId,
|
||||
datasourceUid: rulesSource.uid,
|
||||
queryType: '',
|
||||
model,
|
||||
relativeTimeRange: {
|
||||
from: 360,
|
||||
to: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
return [dataQueryToAlertQuery(model, rulesSource.uid)];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function dataQueryToAlertQuery(dataQuery: DataQuery, dataSourceUid: string): AlertQuery {
|
||||
return {
|
||||
refId: dataQuery.refId,
|
||||
datasourceUid: dataSourceUid,
|
||||
queryType: '',
|
||||
model: dataQuery,
|
||||
relativeTimeRange: {
|
||||
from: 360,
|
||||
to: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function cloudAlertRuleToModel(dsSettings: DataSourceInstanceSettings, rule: CombinedRule): DataQuery {
|
||||
const refId = 'A';
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ type GrafanaAlertStateReason = ` (${string})` | '';
|
|||
|
||||
export type GrafanaAlertStateWithReason = `${GrafanaAlertState}${GrafanaAlertStateReason}`;
|
||||
|
||||
export function isGrafanaAlertState(state: string): state is GrafanaAlertState {
|
||||
return Object.values(GrafanaAlertState).some((promState) => promState === state);
|
||||
}
|
||||
|
||||
/** We need this to disambiguate the union PromAlertingRuleState | GrafanaAlertStateWithReason
|
||||
*/
|
||||
export function isAlertStateWithReason(
|
||||
|
|
|
|||
Loading…
Reference in New Issue