mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: moving data source uid to query instead of model (#33416)
* initial commit. * Some more improvements to the expression data source support. * added tests to verify that time range picker and data source picker only is visible when callbacks is passed to row. * fixing issue with filter in alerting list. * minor refactoring. * removed guarding code, should be fixed in backend. * cleaning the data query if we change to a different data source.
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
export const Components = {
|
export const Components = {
|
||||||
|
TimePicker: {
|
||||||
|
openButton: 'TimePicker Open Button',
|
||||||
|
},
|
||||||
DataSource: {
|
DataSource: {
|
||||||
TestData: {
|
TestData: {
|
||||||
QueryTab: {
|
QueryTab: {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getFocusStyle } from '../Forms/commonStyles';
|
|||||||
import { TimePickerButtonLabel } from './TimeRangePicker';
|
import { TimePickerButtonLabel } from './TimeRangePicker';
|
||||||
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
||||||
import { otherOptions, quickOptions } from './rangeOptions';
|
import { otherOptions, quickOptions } from './rangeOptions';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
const isValidTimeRange = (range: any) => {
|
const isValidTimeRange = (range: any) => {
|
||||||
return dateMath.isValid(range.from) && dateMath.isValid(range.to);
|
return dateMath.isValid(range.from) && dateMath.isValid(range.to);
|
||||||
@@ -66,7 +67,12 @@ export const TimeRangeInput: FC<TimeRangeInputProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div tabIndex={0} className={styles.pickerInput} aria-label="TimePicker Open Button" onClick={onOpen}>
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
className={styles.pickerInput}
|
||||||
|
aria-label={selectors.components.TimePicker.openButton}
|
||||||
|
onClick={onOpen}
|
||||||
|
>
|
||||||
{isValidTimeRange(value) ? (
|
{isValidTimeRange(value) ? (
|
||||||
<TimePickerButtonLabel value={value as TimeRange} timeZone={timeZone} />
|
<TimePickerButtonLabel value={value as TimeRange} timeZone={timeZone} />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { Button, HorizontalGroup, Icon, stylesFactory, Tooltip } from '@grafana/ui';
|
import { Button, HorizontalGroup, Icon, stylesFactory, Tooltip } from '@grafana/ui';
|
||||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { AlertingQueryRows } from './AlertingQueryRows';
|
import { AlertingQueryRows } from './AlertingQueryRows';
|
||||||
import {
|
import { dataSource as expressionDatasource, ExpressionDatasourceUID } from '../../expressions/ExpressionDatasource';
|
||||||
expressionDatasource,
|
|
||||||
ExpressionDatasourceID,
|
|
||||||
ExpressionDatasourceUID,
|
|
||||||
} from '../../expressions/ExpressionDatasource';
|
|
||||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||||
import { defaultCondition } from '../../expressions/utils/expressionTypes';
|
import { defaultCondition } from '../../expressions/utils/expressionTypes';
|
||||||
import { ExpressionQueryType } from '../../expressions/types';
|
import { ExpressionQueryType } from '../../expressions/types';
|
||||||
import { GrafanaQuery, GrafanaQueryModel } from 'app/types/unified-alerting-dto';
|
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: GrafanaQuery[];
|
value?: GrafanaQuery[];
|
||||||
@@ -53,27 +49,29 @@ export class AlertingQueryEditor extends PureComponent<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alertingQuery: GrafanaQueryModel = {
|
onChange(
|
||||||
refId: '',
|
addQuery(value, {
|
||||||
datasourceUid: defaultDataSource.uid,
|
datasourceUid: defaultDataSource.uid,
|
||||||
datasource: defaultDataSource.name,
|
model: {
|
||||||
};
|
refId: '',
|
||||||
|
datasource: defaultDataSource.name,
|
||||||
onChange(addQuery(value, alertingQuery));
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
onNewExpressionQuery = () => {
|
onNewExpressionQuery = () => {
|
||||||
const { onChange, value = [] } = this.props;
|
const { onChange, value = [] } = this.props;
|
||||||
const expressionQuery: GrafanaQueryModel = {
|
|
||||||
...expressionDatasource.newQuery({
|
|
||||||
type: ExpressionQueryType.classic,
|
|
||||||
conditions: [defaultCondition],
|
|
||||||
}),
|
|
||||||
datasourceUid: ExpressionDatasourceUID,
|
|
||||||
datasource: ExpressionDatasourceID,
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange(addQuery(value, expressionQuery));
|
onChange(
|
||||||
|
addQuery(value, {
|
||||||
|
datasourceUid: ExpressionDatasourceUID,
|
||||||
|
model: expressionDatasource.newQuery({
|
||||||
|
type: ExpressionQueryType.classic,
|
||||||
|
conditions: [defaultCondition],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderAddQueryRow(styles: ReturnType<typeof getStyles>) {
|
renderAddQueryRow(styles: ReturnType<typeof getStyles>) {
|
||||||
@@ -123,14 +121,18 @@ export class AlertingQueryEditor extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addQuery = (queries: GrafanaQuery[], model: GrafanaQueryModel): GrafanaQuery[] => {
|
const addQuery = (
|
||||||
|
queries: GrafanaQuery[],
|
||||||
|
queryToAdd: Pick<GrafanaQuery, 'model' | 'datasourceUid'>
|
||||||
|
): GrafanaQuery[] => {
|
||||||
const refId = getNextRefIdChar(queries);
|
const refId = getNextRefIdChar(queries);
|
||||||
|
|
||||||
const query: GrafanaQuery = {
|
const query: GrafanaQuery = {
|
||||||
|
...queryToAdd,
|
||||||
refId,
|
refId,
|
||||||
queryType: '',
|
queryType: '',
|
||||||
model: {
|
model: {
|
||||||
...model,
|
...queryToAdd.model,
|
||||||
hide: false,
|
hide: false,
|
||||||
refId: refId,
|
refId: refId,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
import { DataQuery, DataSourceApi, DataSourceInstanceSettings, rangeUtil, PanelData, TimeRange } from '@grafana/data';
|
import { DataQuery, DataSourceInstanceSettings, rangeUtil, PanelData, TimeRange } from '@grafana/data';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
|
import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
|
||||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||||
@@ -18,18 +18,12 @@ interface Props {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
dataPerQuery: Record<string, PanelData>;
|
dataPerQuery: Record<string, PanelData>;
|
||||||
defaultDataSource: DataSourceApi;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AlertingQueryRows extends PureComponent<Props, State> {
|
export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { dataPerQuery: {}, defaultDataSource: {} as DataSourceApi };
|
this.state = { dataPerQuery: {} };
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const defaultDataSource = await getDataSourceSrv().get();
|
|
||||||
this.setState({ defaultDataSource });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveQuery = (query: DataQuery) => {
|
onRemoveQuery = (query: DataQuery) => {
|
||||||
@@ -40,10 +34,42 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
|||||||
const { queries, onQueriesChange } = this.props;
|
const { queries, onQueriesChange } = this.props;
|
||||||
onQueriesChange(
|
onQueriesChange(
|
||||||
queries.map((item, itemIndex) => {
|
queries.map((item, itemIndex) => {
|
||||||
if (itemIndex === index) {
|
if (itemIndex !== index) {
|
||||||
return { ...item, relativeTimeRange: rangeUtil.timeRangeToRelative(timeRange) };
|
return item;
|
||||||
}
|
}
|
||||||
return item;
|
return {
|
||||||
|
...item,
|
||||||
|
relativeTimeRange: rangeUtil.timeRangeToRelative(timeRange),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeDataSource(settings: DataSourceInstanceSettings, index: number) {
|
||||||
|
const { queries, onQueriesChange } = this.props;
|
||||||
|
|
||||||
|
onQueriesChange(
|
||||||
|
queries.map((item, itemIndex) => {
|
||||||
|
if (itemIndex !== index) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = getDataSourceSrv().getInstanceSettings(item.datasourceUid);
|
||||||
|
|
||||||
|
if (previous?.type === settings.uid) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
datasourceUid: settings.uid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { refId, hide } = item.model;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
datasourceUid: settings.uid,
|
||||||
|
model: { refId, hide },
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -52,10 +78,17 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
|||||||
const { queries, onQueriesChange } = this.props;
|
const { queries, onQueriesChange } = this.props;
|
||||||
onQueriesChange(
|
onQueriesChange(
|
||||||
queries.map((item, itemIndex) => {
|
queries.map((item, itemIndex) => {
|
||||||
if (itemIndex === index) {
|
if (itemIndex !== index) {
|
||||||
return { ...item, model: { ...item.model, ...query, datasource: query.datasource! } };
|
return item;
|
||||||
}
|
}
|
||||||
return item;
|
return {
|
||||||
|
...item,
|
||||||
|
model: {
|
||||||
|
...item.model,
|
||||||
|
...query,
|
||||||
|
datasource: query.datasource!,
|
||||||
|
},
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,14 +112,8 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
|||||||
onQueriesChange(update);
|
onQueriesChange(update);
|
||||||
};
|
};
|
||||||
|
|
||||||
getDataSourceSettings = (query: DataQuery): DataSourceInstanceSettings | undefined => {
|
getDataSourceSettings = (query: GrafanaQuery): DataSourceInstanceSettings | undefined => {
|
||||||
const { defaultDataSource } = this.state;
|
return getDataSourceSrv().getInstanceSettings(query.datasourceUid);
|
||||||
|
|
||||||
if (isExpressionQuery(query)) {
|
|
||||||
return getDataSourceSrv().getInstanceSettings(defaultDataSource.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getDataSourceSrv().getInstanceSettings(query.datasource);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -108,7 +135,12 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryEditorRow
|
<QueryEditorRow
|
||||||
dsSettings={{ ...dsSettings, meta: { ...dsSettings.meta, mixed: true } }}
|
dataSource={dsSettings}
|
||||||
|
onChangeDataSource={
|
||||||
|
!isExpressionQuery(query.model)
|
||||||
|
? (settings) => this.onChangeDataSource(settings, index)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
id={query.refId}
|
id={query.refId}
|
||||||
index={index}
|
index={index}
|
||||||
key={query.refId}
|
key={query.refId}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import React, { FC, useMemo } from 'react';
|
|||||||
import { useStyles } from '@grafana/ui';
|
import { useStyles } from '@grafana/ui';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { isAlertingRule } from '../../utils/rules';
|
import { isAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
import { Annotation } from '../Annotation';
|
import { Annotation } from '../Annotation';
|
||||||
import { AlertLabels } from '../AlertLabels';
|
import { AlertLabels } from '../AlertLabels';
|
||||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||||
import { DetailsField } from '../DetailsField';
|
import { DetailsField } from '../DetailsField';
|
||||||
import { RuleQuery } from './RuleQuery';
|
import { RuleQuery } from './RuleQuery';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
|
import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rule: CombinedRule;
|
rule: CombinedRule;
|
||||||
@@ -27,17 +28,23 @@ export const RuleDetails: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
const dataSources: Array<{ name: string; icon?: string }> = useMemo(() => {
|
const dataSources: Array<{ name: string; icon?: string }> = useMemo(() => {
|
||||||
if (isCloudRulesSource(rulesSource)) {
|
if (isCloudRulesSource(rulesSource)) {
|
||||||
return [{ name: rulesSource.name, icon: rulesSource.meta.info.logos.small }];
|
return [{ name: rulesSource.name, icon: rulesSource.meta.info.logos.small }];
|
||||||
} else if (rule.queries) {
|
|
||||||
return rule.queries
|
|
||||||
.map(({ datasource }) => {
|
|
||||||
const ds = getDataSourceSrv().getInstanceSettings(datasource);
|
|
||||||
if (ds) {
|
|
||||||
return { name: ds.name, icon: ds.meta.info.logos.small };
|
|
||||||
}
|
|
||||||
return { name: datasource };
|
|
||||||
})
|
|
||||||
.filter(({ name }) => name !== '__expr__');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGrafanaRulerRule(rule.rulerRule)) {
|
||||||
|
const { data } = rule.rulerRule.grafana_alert;
|
||||||
|
|
||||||
|
return data.reduce((dataSources, query) => {
|
||||||
|
const ds = getDatasourceSrv().getInstanceSettings(query.datasourceUid);
|
||||||
|
|
||||||
|
if (!ds || ds.uid === ExpressionDatasourceUID) {
|
||||||
|
return dataSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSources.push({ name: ds.name, icon: ds.meta.info.logos.small });
|
||||||
|
return dataSources;
|
||||||
|
}, [] as Array<{ name: string; icon?: string }>);
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}, [rule, rulesSource]);
|
}, [rule, rulesSource]);
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ function promRuleToCombinedRule(rule: Rule, namespace: CombinedRuleNamespace, gr
|
|||||||
return {
|
return {
|
||||||
name: rule.name,
|
name: rule.name,
|
||||||
query: rule.query,
|
query: rule.query,
|
||||||
queries: rule.query && isGrafanaRulesSource(namespace.rulesSource) ? JSON.parse(rule.query) : undefined,
|
|
||||||
labels: rule.labels || {},
|
labels: rule.labels || {},
|
||||||
annotations: isAlertingRule(rule) ? rule.annotations || {} : {},
|
annotations: isAlertingRule(rule) ? rule.annotations || {} : {},
|
||||||
promRule: rule,
|
promRule: rule,
|
||||||
@@ -156,7 +155,6 @@ function rulerRuleToCombinedRule(
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
name: rule.grafana_alert.title,
|
name: rule.grafana_alert.title,
|
||||||
queries: (rule.grafana_alert.data ?? []).map((d) => d.model),
|
|
||||||
query: '',
|
query: '',
|
||||||
labels: rule.labels || {},
|
labels: rule.labels || {},
|
||||||
annotations: rule.annotations || {},
|
annotations: rule.annotations || {},
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleFilterState } from 'app/types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace, RuleFilterState } from 'app/types/unified-alerting';
|
||||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../utils/datasource';
|
import { isCloudRulesSource } from '../utils/datasource';
|
||||||
import { isAlertingRule } from '../utils/rules';
|
import { isAlertingRule, isGrafanaRulerRule } from '../utils/rules';
|
||||||
import { getFiltersFromUrlParams } from '../utils/misc';
|
import { getFiltersFromUrlParams } from '../utils/misc';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
import { RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
export const useFilteredRules = (namespaces: CombinedRuleNamespace[]) => {
|
export const useFilteredRules = (namespaces: CombinedRuleNamespace[]) => {
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
@@ -45,11 +47,7 @@ const reduceNamespaces = (filters: RuleFilterState) => {
|
|||||||
const reduceGroups = (filters: RuleFilterState) => {
|
const reduceGroups = (filters: RuleFilterState) => {
|
||||||
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => {
|
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => {
|
||||||
const rules = group.rules.filter((rule) => {
|
const rules = group.rules.filter((rule) => {
|
||||||
if (
|
if (filters.dataSource && isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filters)) {
|
||||||
filters.dataSource &&
|
|
||||||
isGrafanaRulesSource(rule.namespace.rulesSource) &&
|
|
||||||
!rule.queries?.find(({ datasource }) => datasource === filters.dataSource)
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Query strings can match alert name, label keys, and label values
|
// Query strings can match alert name, label keys, and label values
|
||||||
@@ -84,3 +82,17 @@ const reduceGroups = (filters: RuleFilterState) => {
|
|||||||
return groupAcc;
|
return groupAcc;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isQueryingDataSource = (rulerRule: RulerGrafanaRuleDTO, filter: RuleFilterState): boolean => {
|
||||||
|
if (!filter.dataSource) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!rulerRule.grafana_alert.data.find((query) => {
|
||||||
|
if (!query.datasourceUid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ds = getDataSourceSrv().getInstanceSettings(query.datasourceUid);
|
||||||
|
return ds?.name === filter.dataSource;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ export const SAMPLE_QUERIES = [
|
|||||||
from: 30,
|
from: 30,
|
||||||
to: 0,
|
to: 0,
|
||||||
},
|
},
|
||||||
|
datasourceUid: '000000004',
|
||||||
model: {
|
model: {
|
||||||
datasource: 'gdev-testdata',
|
|
||||||
datasourceUid: '000000004',
|
|
||||||
intervalMs: 1000,
|
intervalMs: 1000,
|
||||||
maxDataPoints: 100,
|
maxDataPoints: 100,
|
||||||
pulseWave: {
|
pulseWave: {
|
||||||
@@ -30,6 +29,7 @@ export const SAMPLE_QUERIES = [
|
|||||||
from: 0,
|
from: 0,
|
||||||
to: 0,
|
to: 0,
|
||||||
},
|
},
|
||||||
|
datasourceUid: '-100',
|
||||||
model: {
|
model: {
|
||||||
conditions: [
|
conditions: [
|
||||||
{
|
{
|
||||||
@@ -48,8 +48,6 @@ export const SAMPLE_QUERIES = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
datasource: '__expr__',
|
|
||||||
datasourceUid: '-100',
|
|
||||||
intervalMs: 1000,
|
intervalMs: 1000,
|
||||||
maxDataPoints: 100,
|
maxDataPoints: 100,
|
||||||
refId: 'B',
|
refId: 'B',
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
|||||||
...defaultFormValues,
|
...defaultFormValues,
|
||||||
name: ga.title,
|
name: ga.title,
|
||||||
type: RuleFormType.threshold,
|
type: RuleFormType.threshold,
|
||||||
dataSourceName: ga.data[0]?.model.datasource,
|
|
||||||
evaluateFor: rule.for,
|
evaluateFor: rule.for,
|
||||||
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
||||||
noDataState: ga.no_data_state,
|
noDataState: ga.no_data_state,
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export function isRecordingRulerRule(rule: RulerRuleDTO): rule is RulerRecording
|
|||||||
return 'record' in rule;
|
return 'record' in rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isGrafanaRulerRule(rule: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
|
export function isGrafanaRulerRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
|
||||||
return 'grafana_alert' in rule;
|
return typeof rule === 'object' && 'grafana_alert' in rule;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function alertInstanceKey(alert: Alert): string {
|
export function alertInstanceKey(alert: Alert): string {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
|
import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginType } from '@grafana/data';
|
||||||
import { ExpressionQuery, ExpressionQueryType } from './types';
|
import { ExpressionQuery, ExpressionQueryType } from './types';
|
||||||
import { ExpressionQueryEditor } from './ExpressionQueryEditor';
|
import { ExpressionQueryEditor } from './ExpressionQueryEditor';
|
||||||
import { DataSourceWithBackend } from '@grafana/runtime';
|
import { DataSourceWithBackend } from '@grafana/runtime';
|
||||||
@@ -29,11 +29,37 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue
|
|||||||
export const ExpressionDatasourceID = '__expr__';
|
export const ExpressionDatasourceID = '__expr__';
|
||||||
export const ExpressionDatasourceUID = '-100';
|
export const ExpressionDatasourceUID = '-100';
|
||||||
|
|
||||||
export const expressionDatasource = new ExpressionDatasourceApi({
|
export const instanceSettings: DataSourceInstanceSettings = {
|
||||||
id: -100,
|
id: -100,
|
||||||
|
uid: ExpressionDatasourceUID,
|
||||||
name: ExpressionDatasourceID,
|
name: ExpressionDatasourceID,
|
||||||
} as DataSourceInstanceSettings);
|
type: 'grafana-expression',
|
||||||
expressionDatasource.meta = {
|
meta: {
|
||||||
|
baseUrl: '',
|
||||||
|
module: '',
|
||||||
|
type: PluginType.datasource,
|
||||||
|
name: ExpressionDatasourceID,
|
||||||
|
id: ExpressionDatasourceID,
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'Grafana Labs',
|
||||||
|
},
|
||||||
|
logos: {
|
||||||
|
small: 'public/img/icn-datasource.svg',
|
||||||
|
large: 'public/img/icn-datasource.svg',
|
||||||
|
},
|
||||||
|
description: 'Adds expression support to Grafana',
|
||||||
|
screenshots: [],
|
||||||
|
links: [],
|
||||||
|
updated: '',
|
||||||
|
version: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
jsonData: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dataSource = new ExpressionDatasourceApi(instanceSettings);
|
||||||
|
dataSource.meta = {
|
||||||
id: ExpressionDatasourceID,
|
id: ExpressionDatasourceID,
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
@@ -42,6 +68,6 @@ expressionDatasource.meta = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as DataSourcePluginMeta;
|
} as DataSourcePluginMeta;
|
||||||
expressionDatasource.components = {
|
dataSource.components = {
|
||||||
QueryEditor: ExpressionQueryEditor,
|
QueryEditor: ExpressionQueryEditor,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ import { AppEvents, DataSourceApi, DataSourceInstanceSettings, DataSourceSelectI
|
|||||||
import { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
// Pretend Datasource
|
// Pretend Datasource
|
||||||
import { expressionDatasource } from 'app/features/expressions/ExpressionDatasource';
|
import {
|
||||||
|
dataSource as expressionDatasource,
|
||||||
|
ExpressionDatasourceID,
|
||||||
|
ExpressionDatasourceUID,
|
||||||
|
instanceSettings as expressionInstanceSettings,
|
||||||
|
} from 'app/features/expressions/ExpressionDatasource';
|
||||||
import { DataSourceVariableModel } from '../variables/types';
|
import { DataSourceVariableModel } from '../variables/types';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
@@ -52,6 +57,10 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
return this.settingsMapByName[this.defaultName];
|
return this.settingsMapByName[this.defaultName];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nameOrUid === ExpressionDatasourceID || nameOrUid === ExpressionDatasourceUID) {
|
||||||
|
return expressionInstanceSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// Complex logic to support template variable data source names
|
// Complex logic to support template variable data source names
|
||||||
// For this we just pick the current or first data source in the variable
|
// For this we just pick the current or first data source in the variable
|
||||||
if (nameOrUid[0] === '$') {
|
if (nameOrUid[0] === '$') {
|
||||||
@@ -112,7 +121,7 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
|
|
||||||
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
|
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
|
||||||
// Expression Datasource (not a real datasource)
|
// Expression Datasource (not a real datasource)
|
||||||
if (name === expressionDatasource.name) {
|
if (name === ExpressionDatasourceID || name === ExpressionDatasourceUID) {
|
||||||
this.datasources[name] = expressionDatasource as any;
|
this.datasources[name] = expressionDatasource as any;
|
||||||
return Promise.resolve(expressionDatasource);
|
return Promise.resolve(expressionDatasource);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
TimeRange,
|
TimeRange,
|
||||||
toLegacyResponseData,
|
toLegacyResponseData,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { QueryEditorRowTitle } from './QueryEditorRowTitle';
|
import { QueryEditorRowHeader } from './QueryEditorRowHeader';
|
||||||
import {
|
import {
|
||||||
QueryOperationRow,
|
QueryOperationRow,
|
||||||
QueryOperationRowRenderProps,
|
QueryOperationRowRenderProps,
|
||||||
@@ -33,10 +33,11 @@ interface Props {
|
|||||||
data: PanelData;
|
data: PanelData;
|
||||||
query: DataQuery;
|
query: DataQuery;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
dsSettings: DataSourceInstanceSettings;
|
|
||||||
id: string;
|
id: string;
|
||||||
index: number;
|
index: number;
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
|
dataSource: DataSourceInstanceSettings;
|
||||||
|
onChangeDataSource?: (dsSettings: DataSourceInstanceSettings) => void;
|
||||||
onChangeTimeRange?: (timeRange: TimeRange) => void;
|
onChangeTimeRange?: (timeRange: TimeRange) => void;
|
||||||
onAddQuery: (query: DataQuery) => void;
|
onAddQuery: (query: DataQuery) => void;
|
||||||
onRemoveQuery: (query: DataQuery) => void;
|
onRemoveQuery: (query: DataQuery) => void;
|
||||||
@@ -111,7 +112,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getQueryDataSourceIdentifier(): string | null | undefined {
|
getQueryDataSourceIdentifier(): string | null | undefined {
|
||||||
const { query, dsSettings } = this.props;
|
const { query, dataSource: dsSettings } = this.props;
|
||||||
return query.datasource ?? dsSettings.name;
|
return query.datasource ?? dsSettings.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,20 +303,18 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
renderTitle = (props: QueryOperationRowRenderProps) => {
|
renderHeader = (props: QueryOperationRowRenderProps) => {
|
||||||
const { query, dsSettings, onChange, queries, onChangeTimeRange, timeRange } = this.props;
|
const { query, dataSource, onChangeDataSource, onChange, queries, onChangeTimeRange, timeRange } = this.props;
|
||||||
const { datasource } = this.state;
|
|
||||||
const isDisabled = query.hide;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryEditorRowTitle
|
<QueryEditorRowHeader
|
||||||
query={query}
|
query={query}
|
||||||
queries={queries}
|
queries={queries}
|
||||||
onTimeRangeChange={onChangeTimeRange}
|
onChangeTimeRange={onChangeTimeRange}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
inMixedMode={dsSettings.meta.mixed}
|
onChangeDataSource={onChangeDataSource}
|
||||||
dataSourceName={datasource!.name}
|
dataSource={dataSource}
|
||||||
disabled={isDisabled}
|
disabled={query.hide}
|
||||||
onClick={(e) => this.onToggleEditMode(e, props)}
|
onClick={(e) => this.onToggleEditMode(e, props)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
collapsedText={!props.isOpen ? this.renderCollapsedText() : null}
|
collapsedText={!props.isOpen ? this.renderCollapsedText() : null}
|
||||||
@@ -346,7 +345,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
|||||||
id={id}
|
id={id}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
index={index}
|
index={index}
|
||||||
headerElement={this.renderTitle}
|
headerElement={this.renderHeader}
|
||||||
actions={this.renderActions}
|
actions={this.renderActions}
|
||||||
onOpen={this.onOpen}
|
onOpen={this.onOpen}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { Props, QueryEditorRowHeader } from './QueryEditorRowHeader';
|
||||||
|
import { DataSourceInstanceSettings, dateTime } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
|
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
|
||||||
|
return {
|
||||||
|
getDataSourceSrv: () => ({
|
||||||
|
getInstanceSettings: jest.fn(),
|
||||||
|
getList: jest.fn().mockReturnValue([]),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QueryEditorRowHeader', () => {
|
||||||
|
it('Can edit title', () => {
|
||||||
|
const scenario = renderScenario({});
|
||||||
|
screen.getByTestId('query-name-div').click();
|
||||||
|
|
||||||
|
const input = screen.getByTestId('query-name-input');
|
||||||
|
fireEvent.change(input, { target: { value: 'new name' } });
|
||||||
|
fireEvent.blur(input);
|
||||||
|
|
||||||
|
expect((scenario.props.onChange as any).mock.calls[0][0].refId).toBe('new name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Show error when other query with same name exists', async () => {
|
||||||
|
renderScenario({});
|
||||||
|
|
||||||
|
screen.getByTestId('query-name-div').click();
|
||||||
|
const input = screen.getByTestId('query-name-input');
|
||||||
|
fireEvent.change(input, { target: { value: 'B' } });
|
||||||
|
const alert = await screen.findByRole('alert');
|
||||||
|
|
||||||
|
expect(alert.textContent).toBe('Query name already exists');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Show error when empty name is specified', async () => {
|
||||||
|
renderScenario({});
|
||||||
|
|
||||||
|
screen.getByTestId('query-name-div').click();
|
||||||
|
const input = screen.getByTestId('query-name-input');
|
||||||
|
fireEvent.change(input, { target: { value: '' } });
|
||||||
|
const alert = await screen.findByRole('alert');
|
||||||
|
|
||||||
|
expect(alert.textContent).toBe('An empty query name is not allowed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show data source picker when callback is passed', async () => {
|
||||||
|
renderScenario({ onChangeDataSource: () => {} });
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(selectors.components.DataSourcePicker.container)).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show data source picker when no callback is passed', async () => {
|
||||||
|
renderScenario({ onChangeDataSource: undefined });
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(selectors.components.DataSourcePicker.container)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show time range picker when callback and value is passed', async () => {
|
||||||
|
renderScenario({
|
||||||
|
onChangeTimeRange: () => {},
|
||||||
|
timeRange: {
|
||||||
|
from: dateTime(),
|
||||||
|
to: dateTime(),
|
||||||
|
raw: { from: 'now', to: 'now' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(selectors.components.TimePicker.openButton)).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show time range picker when no value is passed', async () => {
|
||||||
|
renderScenario({
|
||||||
|
onChangeTimeRange: () => {},
|
||||||
|
timeRange: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(selectors.components.DataSourcePicker.container)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show time range picker when no callback is passed', async () => {
|
||||||
|
renderScenario({
|
||||||
|
onChangeTimeRange: undefined,
|
||||||
|
timeRange: {
|
||||||
|
from: dateTime(),
|
||||||
|
to: dateTime(),
|
||||||
|
raw: { from: 'now', to: 'now' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(selectors.components.DataSourcePicker.container)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderScenario(overrides: Partial<Props>) {
|
||||||
|
const props: Props = {
|
||||||
|
query: {
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
queries: [
|
||||||
|
{
|
||||||
|
refId: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refId: 'B',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dataSource: {} as DataSourceInstanceSettings,
|
||||||
|
disabled: false,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
onClick: jest.fn(),
|
||||||
|
collapsedText: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, overrides);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props,
|
||||||
|
renderResult: render(<QueryEditorRowHeader {...props} />),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,37 +2,27 @@ import React, { useState } from 'react';
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { DataQuery, DataSourceInstanceSettings, GrafanaTheme, TimeRange } from '@grafana/data';
|
import { DataQuery, DataSourceInstanceSettings, GrafanaTheme, TimeRange } from '@grafana/data';
|
||||||
import { DataSourcePicker } from '@grafana/runtime';
|
import { DataSourcePicker } from '@grafana/runtime';
|
||||||
import { Icon, Input, stylesFactory, useTheme, FieldValidationMessage, TimeRangeInput } from '@grafana/ui';
|
import { Icon, Input, FieldValidationMessage, TimeRangeInput, useStyles } from '@grafana/ui';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource';
|
import { ExpressionDatasourceUID } from '../../expressions/ExpressionDatasource';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
query: DataQuery;
|
query: DataQuery;
|
||||||
queries: DataQuery[];
|
queries: DataQuery[];
|
||||||
dataSourceName: string;
|
|
||||||
inMixedMode?: boolean;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
onTimeRangeChange?: (timeRange: TimeRange) => void;
|
dataSource: DataSourceInstanceSettings;
|
||||||
|
onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
|
||||||
|
onChangeTimeRange?: (timeRange: TimeRange) => void;
|
||||||
onChange: (query: DataQuery) => void;
|
onChange: (query: DataQuery) => void;
|
||||||
onClick: (e: React.MouseEvent) => void;
|
onClick: (e: React.MouseEvent) => void;
|
||||||
collapsedText: string | null;
|
collapsedText: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QueryEditorRowTitle: React.FC<Props> = ({
|
export const QueryEditorRowHeader: React.FC<Props> = (props) => {
|
||||||
dataSourceName,
|
const { dataSource, onChangeDataSource, disabled, query, queries, onClick, onChange, collapsedText } = props;
|
||||||
inMixedMode,
|
|
||||||
disabled,
|
const styles = useStyles(getStyles);
|
||||||
query,
|
|
||||||
queries,
|
|
||||||
onClick,
|
|
||||||
onChange,
|
|
||||||
onTimeRangeChange,
|
|
||||||
timeRange,
|
|
||||||
collapsedText,
|
|
||||||
}) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const styles = getQueryEditorRowTitleStyles(theme);
|
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const [validationError, setValidationError] = useState<string | null>(null);
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -91,10 +81,6 @@ export const QueryEditorRowTitle: React.FC<Props> = ({
|
|||||||
event.target.select();
|
event.target.select();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDataSourceChange = (dataSource: DataSourceInstanceSettings) => {
|
|
||||||
onChange({ ...query, datasource: dataSource.name });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
@@ -126,17 +112,8 @@ export const QueryEditorRowTitle: React.FC<Props> = ({
|
|||||||
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
|
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{inMixedMode && (
|
<PickerRenderer {...props} />
|
||||||
<div style={{ display: 'flex', marginLeft: '8px' }}>
|
{dataSource && !onChangeDataSource && <em className={styles.contextInfo}> ({dataSource.name})</em>}
|
||||||
{query.datasource !== ExpressionDatasourceID && (
|
|
||||||
<>
|
|
||||||
<DataSourcePicker current={dataSourceName} onChange={onDataSourceChange} />
|
|
||||||
{onTimeRangeChange && timeRange && <TimeRangeInput onChange={onTimeRangeChange} value={timeRange} />}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{dataSourceName && !inMixedMode && <em className={styles.contextInfo}> ({dataSourceName})</em>}
|
|
||||||
{disabled && <em className={styles.contextInfo}> Disabled</em>}
|
{disabled && <em className={styles.contextInfo}> Disabled</em>}
|
||||||
|
|
||||||
{collapsedText && (
|
{collapsedText && (
|
||||||
@@ -148,7 +125,27 @@ export const QueryEditorRowTitle: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getQueryEditorRowTitleStyles = stylesFactory((theme: GrafanaTheme) => {
|
const PickerRenderer: React.FC<Props> = (props) => {
|
||||||
|
const { onChangeTimeRange, timeRange, onChangeDataSource, dataSource } = props;
|
||||||
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
if (!onChangeTimeRange && !onChangeDataSource) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataSource.uid === ExpressionDatasourceUID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.pickerWrapper}>
|
||||||
|
{onChangeDataSource && <DataSourcePicker current={dataSource.name} onChange={onChangeDataSource} />}
|
||||||
|
{onChangeTimeRange && timeRange && <TimeRangeInput onChange={onChangeTimeRange} value={timeRange} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
wrapper: css`
|
wrapper: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -225,5 +222,9 @@ const getQueryEditorRowTitleStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
color: ${theme.colors.textWeak};
|
color: ${theme.colors.textWeak};
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
`,
|
`,
|
||||||
|
pickerWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
margin-left: 8px;
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
|
||||||
import { Props, QueryEditorRowTitle } from './QueryEditorRowTitle';
|
|
||||||
|
|
||||||
function renderScenario(overrides: Partial<Props>) {
|
|
||||||
const props: Props = {
|
|
||||||
query: {
|
|
||||||
refId: 'A',
|
|
||||||
},
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
refId: 'A',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refId: 'B',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dataSourceName: 'hello',
|
|
||||||
disabled: false,
|
|
||||||
onChange: jest.fn(),
|
|
||||||
onClick: jest.fn(),
|
|
||||||
collapsedText: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.assign(props, overrides);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props,
|
|
||||||
renderResult: render(<QueryEditorRowTitle {...props} />),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('QueryEditorRowTitle', () => {
|
|
||||||
it('Can edit title', () => {
|
|
||||||
const scenario = renderScenario({});
|
|
||||||
screen.getByTestId('query-name-div').click();
|
|
||||||
|
|
||||||
const input = screen.getByTestId('query-name-input');
|
|
||||||
fireEvent.change(input, { target: { value: 'new name' } });
|
|
||||||
fireEvent.blur(input);
|
|
||||||
|
|
||||||
expect((scenario.props.onChange as any).mock.calls[0][0].refId).toBe('new name');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Show error when other query with same name exists', async () => {
|
|
||||||
renderScenario({});
|
|
||||||
|
|
||||||
screen.getByTestId('query-name-div').click();
|
|
||||||
const input = screen.getByTestId('query-name-input');
|
|
||||||
fireEvent.change(input, { target: { value: 'B' } });
|
|
||||||
const alert = await screen.findByRole('alert');
|
|
||||||
|
|
||||||
expect(alert.textContent).toBe('Query name already exists');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Show error when empty name is specified', async () => {
|
|
||||||
renderScenario({});
|
|
||||||
|
|
||||||
screen.getByTestId('query-name-div').click();
|
|
||||||
const input = screen.getByTestId('query-name-input');
|
|
||||||
fireEvent.change(input, { target: { value: '' } });
|
|
||||||
const alert = await screen.findByRole('alert');
|
|
||||||
|
|
||||||
expect(alert.textContent).toBe('An empty query name is not allowed');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -5,6 +5,7 @@ import React, { PureComponent } from 'react';
|
|||||||
import { DataQuery, DataSourceInstanceSettings, PanelData } from '@grafana/data';
|
import { DataQuery, DataSourceInstanceSettings, PanelData } from '@grafana/data';
|
||||||
import { QueryEditorRow } from './QueryEditorRow';
|
import { QueryEditorRow } from './QueryEditorRow';
|
||||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// The query configuration
|
// The query configuration
|
||||||
@@ -39,6 +40,35 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDataSourceChange(dataSource: DataSourceInstanceSettings, index: number) {
|
||||||
|
const { queries, onQueriesChange } = this.props;
|
||||||
|
|
||||||
|
onQueriesChange(
|
||||||
|
queries.map((item, itemIndex) => {
|
||||||
|
if (itemIndex !== index) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.datasource) {
|
||||||
|
const previous = getDataSourceSrv().getInstanceSettings(item.datasource);
|
||||||
|
|
||||||
|
if (previous?.type === dataSource.type) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
datasource: dataSource.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
refId: item.refId,
|
||||||
|
hide: item.hide,
|
||||||
|
datasource: dataSource.name,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
onDragEnd = (result: DropResult) => {
|
onDragEnd = (result: DropResult) => {
|
||||||
const { queries, onQueriesChange } = this.props;
|
const { queries, onQueriesChange } = this.props;
|
||||||
|
|
||||||
@@ -67,21 +97,29 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
{(provided) => {
|
{(provided) => {
|
||||||
return (
|
return (
|
||||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
{queries.map((query, index) => (
|
{queries.map((query, index) => {
|
||||||
<QueryEditorRow
|
const dataSourceSettings = getDataSourceSettings(query, dsSettings);
|
||||||
dsSettings={dsSettings}
|
const onChangeDataSourceSettings = dsSettings.meta.mixed
|
||||||
id={query.refId}
|
? (settings: DataSourceInstanceSettings) => this.onDataSourceChange(settings, index)
|
||||||
index={index}
|
: undefined;
|
||||||
key={query.refId}
|
|
||||||
data={data}
|
return (
|
||||||
query={query}
|
<QueryEditorRow
|
||||||
onChange={(query) => this.onChangeQuery(query, index)}
|
id={query.refId}
|
||||||
onRemoveQuery={this.onRemoveQuery}
|
index={index}
|
||||||
onAddQuery={this.props.onAddQuery}
|
key={query.refId}
|
||||||
onRunQuery={this.props.onRunQueries}
|
data={data}
|
||||||
queries={queries}
|
query={query}
|
||||||
/>
|
dataSource={dataSourceSettings}
|
||||||
))}
|
onChangeDataSource={onChangeDataSourceSettings}
|
||||||
|
onChange={(query) => this.onChangeQuery(query, index)}
|
||||||
|
onRemoveQuery={this.onRemoveQuery}
|
||||||
|
onAddQuery={this.props.onAddQuery}
|
||||||
|
onRunQuery={this.props.onRunQueries}
|
||||||
|
queries={queries}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -91,3 +129,14 @@ export class QueryEditorRows extends PureComponent<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDataSourceSettings = (
|
||||||
|
query: DataQuery,
|
||||||
|
groupSettings: DataSourceInstanceSettings
|
||||||
|
): DataSourceInstanceSettings => {
|
||||||
|
if (!query.datasource) {
|
||||||
|
return groupSettings;
|
||||||
|
}
|
||||||
|
const querySettings = getDataSourceSrv().getInstanceSettings(query.datasource);
|
||||||
|
return querySettings || groupSettings;
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ import {
|
|||||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||||
import { addQuery } from 'app/core/utils/query';
|
import { addQuery } from 'app/core/utils/query';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
|
import {
|
||||||
|
dataSource as expressionDatasource,
|
||||||
|
ExpressionDatasourceID,
|
||||||
|
} from 'app/features/expressions/ExpressionDatasource';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { PanelQueryRunner } from '../state/PanelQueryRunner';
|
import { PanelQueryRunner } from '../state/PanelQueryRunner';
|
||||||
import { QueryGroupOptionsEditor } from './QueryGroupOptions';
|
import { QueryGroupOptionsEditor } from './QueryGroupOptions';
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { toDataQueryError } from '@grafana/runtime';
|
import { toDataQueryError } from '@grafana/runtime';
|
||||||
import { emitDataRequestEvent } from './queryAnalytics';
|
import { emitDataRequestEvent } from './queryAnalytics';
|
||||||
import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
|
import {
|
||||||
|
dataSource as expressionDatasource,
|
||||||
|
ExpressionDatasourceID,
|
||||||
|
ExpressionDatasourceUID,
|
||||||
|
} from 'app/features/expressions/ExpressionDatasource';
|
||||||
import { ExpressionQuery } from 'app/features/expressions/types';
|
import { ExpressionQuery } from 'app/features/expressions/types';
|
||||||
|
|
||||||
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
||||||
@@ -175,7 +179,7 @@ export function callQueryMethod(
|
|||||||
) {
|
) {
|
||||||
// If any query has an expression, use the expression endpoint
|
// If any query has an expression, use the expression endpoint
|
||||||
for (const target of request.targets) {
|
for (const target of request.targets) {
|
||||||
if (target.datasource === ExpressionDatasourceID) {
|
if (target.datasource === ExpressionDatasourceID || target.datasource === ExpressionDatasourceUID) {
|
||||||
return expressionDatasource.query(request as DataQueryRequest<ExpressionQuery>);
|
return expressionDatasource.query(request as DataQueryRequest<ExpressionQuery>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,16 +92,12 @@ export enum GrafanaAlertState {
|
|||||||
KeepLastState = 'KeepLastState',
|
KeepLastState = 'KeepLastState',
|
||||||
OK = 'OK',
|
OK = 'OK',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GrafanaQueryModel extends DataQuery {
|
|
||||||
datasource: string;
|
|
||||||
datasourceUid: string;
|
|
||||||
}
|
|
||||||
export interface GrafanaQuery {
|
export interface GrafanaQuery {
|
||||||
refId: string;
|
refId: string;
|
||||||
queryType: string;
|
queryType: string;
|
||||||
relativeTimeRange: RelativeTimeRange;
|
relativeTimeRange: RelativeTimeRange;
|
||||||
model: GrafanaQueryModel;
|
datasourceUid: string;
|
||||||
|
model: DataQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostableGrafanaRuleDefinition {
|
export interface PostableGrafanaRuleDefinition {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Labels,
|
Labels,
|
||||||
Annotations,
|
Annotations,
|
||||||
RulerRuleGroupDTO,
|
RulerRuleGroupDTO,
|
||||||
GrafanaQueryModel,
|
|
||||||
} from './unified-alerting-dto';
|
} from './unified-alerting-dto';
|
||||||
|
|
||||||
export type Alert = {
|
export type Alert = {
|
||||||
@@ -82,7 +81,6 @@ export interface CombinedRule {
|
|||||||
rulerRule?: RulerRuleDTO;
|
rulerRule?: RulerRuleDTO;
|
||||||
group: CombinedRuleGroup;
|
group: CombinedRuleGroup;
|
||||||
namespace: CombinedRuleNamespace;
|
namespace: CombinedRuleNamespace;
|
||||||
queries?: GrafanaQueryModel[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CombinedRuleGroup {
|
export interface CombinedRuleGroup {
|
||||||
|
|||||||
Reference in New Issue
Block a user