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:
parent
28ec96788c
commit
9de2f1bb8f
@ -1,4 +1,7 @@
|
||||
export const Components = {
|
||||
TimePicker: {
|
||||
openButton: 'TimePicker Open Button',
|
||||
},
|
||||
DataSource: {
|
||||
TestData: {
|
||||
QueryTab: {
|
||||
|
@ -9,6 +9,7 @@ import { getFocusStyle } from '../Forms/commonStyles';
|
||||
import { TimePickerButtonLabel } from './TimeRangePicker';
|
||||
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
|
||||
import { otherOptions, quickOptions } from './rangeOptions';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
const isValidTimeRange = (range: any) => {
|
||||
return dateMath.isValid(range.from) && dateMath.isValid(range.to);
|
||||
@ -66,7 +67,12 @@ export const TimeRangeInput: FC<TimeRangeInputProps> = ({
|
||||
|
||||
return (
|
||||
<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) ? (
|
||||
<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 { config, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { AlertingQueryRows } from './AlertingQueryRows';
|
||||
import {
|
||||
expressionDatasource,
|
||||
ExpressionDatasourceID,
|
||||
ExpressionDatasourceUID,
|
||||
} from '../../expressions/ExpressionDatasource';
|
||||
import { dataSource as expressionDatasource, ExpressionDatasourceUID } from '../../expressions/ExpressionDatasource';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { defaultCondition } from '../../expressions/utils/expressionTypes';
|
||||
import { ExpressionQueryType } from '../../expressions/types';
|
||||
import { GrafanaQuery, GrafanaQueryModel } from 'app/types/unified-alerting-dto';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
|
||||
interface Props {
|
||||
value?: GrafanaQuery[];
|
||||
@ -53,27 +49,29 @@ export class AlertingQueryEditor extends PureComponent<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
const alertingQuery: GrafanaQueryModel = {
|
||||
refId: '',
|
||||
datasourceUid: defaultDataSource.uid,
|
||||
datasource: defaultDataSource.name,
|
||||
};
|
||||
|
||||
onChange(addQuery(value, alertingQuery));
|
||||
onChange(
|
||||
addQuery(value, {
|
||||
datasourceUid: defaultDataSource.uid,
|
||||
model: {
|
||||
refId: '',
|
||||
datasource: defaultDataSource.name,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onNewExpressionQuery = () => {
|
||||
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>) {
|
||||
@ -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 query: GrafanaQuery = {
|
||||
...queryToAdd,
|
||||
refId,
|
||||
queryType: '',
|
||||
model: {
|
||||
...model,
|
||||
...queryToAdd.model,
|
||||
hide: false,
|
||||
refId: refId,
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
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 { QueryEditorRow } from 'app/features/query/components/QueryEditorRow';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
@ -18,18 +18,12 @@ interface Props {
|
||||
|
||||
interface State {
|
||||
dataPerQuery: Record<string, PanelData>;
|
||||
defaultDataSource: DataSourceApi;
|
||||
}
|
||||
|
||||
export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { dataPerQuery: {}, defaultDataSource: {} as DataSourceApi };
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const defaultDataSource = await getDataSourceSrv().get();
|
||||
this.setState({ defaultDataSource });
|
||||
this.state = { dataPerQuery: {} };
|
||||
}
|
||||
|
||||
onRemoveQuery = (query: DataQuery) => {
|
||||
@ -40,10 +34,42 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
const { queries, onQueriesChange } = this.props;
|
||||
onQueriesChange(
|
||||
queries.map((item, itemIndex) => {
|
||||
if (itemIndex === index) {
|
||||
return { ...item, relativeTimeRange: rangeUtil.timeRangeToRelative(timeRange) };
|
||||
if (itemIndex !== index) {
|
||||
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;
|
||||
onQueriesChange(
|
||||
queries.map((item, itemIndex) => {
|
||||
if (itemIndex === index) {
|
||||
return { ...item, model: { ...item.model, ...query, datasource: query.datasource! } };
|
||||
if (itemIndex !== index) {
|
||||
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);
|
||||
};
|
||||
|
||||
getDataSourceSettings = (query: DataQuery): DataSourceInstanceSettings | undefined => {
|
||||
const { defaultDataSource } = this.state;
|
||||
|
||||
if (isExpressionQuery(query)) {
|
||||
return getDataSourceSrv().getInstanceSettings(defaultDataSource.name);
|
||||
}
|
||||
|
||||
return getDataSourceSrv().getInstanceSettings(query.datasource);
|
||||
getDataSourceSettings = (query: GrafanaQuery): DataSourceInstanceSettings | undefined => {
|
||||
return getDataSourceSrv().getInstanceSettings(query.datasourceUid);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -108,7 +135,12 @@ export class AlertingQueryRows extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<QueryEditorRow
|
||||
dsSettings={{ ...dsSettings, meta: { ...dsSettings.meta, mixed: true } }}
|
||||
dataSource={dsSettings}
|
||||
onChangeDataSource={
|
||||
!isExpressionQuery(query.model)
|
||||
? (settings) => this.onChangeDataSource(settings, index)
|
||||
: undefined
|
||||
}
|
||||
id={query.refId}
|
||||
index={index}
|
||||
key={query.refId}
|
||||
|
@ -3,14 +3,15 @@ import React, { FC, useMemo } from 'react';
|
||||
import { useStyles } from '@grafana/ui';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { isAlertingRule } from '../../utils/rules';
|
||||
import { isAlertingRule, isGrafanaRulerRule } from '../../utils/rules';
|
||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { Annotation } from '../Annotation';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { AlertInstancesTable } from './AlertInstancesTable';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
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 {
|
||||
rule: CombinedRule;
|
||||
@ -27,17 +28,23 @@ export const RuleDetails: FC<Props> = ({ rule, rulesSource }) => {
|
||||
const dataSources: Array<{ name: string; icon?: string }> = useMemo(() => {
|
||||
if (isCloudRulesSource(rulesSource)) {
|
||||
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 [];
|
||||
}, [rule, rulesSource]);
|
||||
|
||||
|
@ -120,7 +120,6 @@ function promRuleToCombinedRule(rule: Rule, namespace: CombinedRuleNamespace, gr
|
||||
return {
|
||||
name: rule.name,
|
||||
query: rule.query,
|
||||
queries: rule.query && isGrafanaRulesSource(namespace.rulesSource) ? JSON.parse(rule.query) : undefined,
|
||||
labels: rule.labels || {},
|
||||
annotations: isAlertingRule(rule) ? rule.annotations || {} : {},
|
||||
promRule: rule,
|
||||
@ -156,7 +155,6 @@ function rulerRuleToCombinedRule(
|
||||
}
|
||||
: {
|
||||
name: rule.grafana_alert.title,
|
||||
queries: (rule.grafana_alert.data ?? []).map((d) => d.model),
|
||||
query: '',
|
||||
labels: rule.labels || {},
|
||||
annotations: rule.annotations || {},
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace, RuleFilterState } from 'app/types/unified-alerting';
|
||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../utils/datasource';
|
||||
import { isAlertingRule } from '../utils/rules';
|
||||
import { isCloudRulesSource } from '../utils/datasource';
|
||||
import { isAlertingRule, isGrafanaRulerRule } from '../utils/rules';
|
||||
import { getFiltersFromUrlParams } from '../utils/misc';
|
||||
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[]) => {
|
||||
const [queryParams] = useQueryParams();
|
||||
@ -45,11 +47,7 @@ const reduceNamespaces = (filters: RuleFilterState) => {
|
||||
const reduceGroups = (filters: RuleFilterState) => {
|
||||
return (groupAcc: CombinedRuleGroup[], group: CombinedRuleGroup) => {
|
||||
const rules = group.rules.filter((rule) => {
|
||||
if (
|
||||
filters.dataSource &&
|
||||
isGrafanaRulesSource(rule.namespace.rulesSource) &&
|
||||
!rule.queries?.find(({ datasource }) => datasource === filters.dataSource)
|
||||
) {
|
||||
if (filters.dataSource && isGrafanaRulerRule(rule.rulerRule) && !isQueryingDataSource(rule.rulerRule, filters)) {
|
||||
return false;
|
||||
}
|
||||
// Query strings can match alert name, label keys, and label values
|
||||
@ -84,3 +82,17 @@ const reduceGroups = (filters: RuleFilterState) => {
|
||||
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,
|
||||
to: 0,
|
||||
},
|
||||
datasourceUid: '000000004',
|
||||
model: {
|
||||
datasource: 'gdev-testdata',
|
||||
datasourceUid: '000000004',
|
||||
intervalMs: 1000,
|
||||
maxDataPoints: 100,
|
||||
pulseWave: {
|
||||
@ -30,6 +29,7 @@ export const SAMPLE_QUERIES = [
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
datasourceUid: '-100',
|
||||
model: {
|
||||
conditions: [
|
||||
{
|
||||
@ -48,8 +48,6 @@ export const SAMPLE_QUERIES = [
|
||||
},
|
||||
},
|
||||
],
|
||||
datasource: '__expr__',
|
||||
datasourceUid: '-100',
|
||||
intervalMs: 1000,
|
||||
maxDataPoints: 100,
|
||||
refId: 'B',
|
||||
|
@ -86,7 +86,6 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
||||
...defaultFormValues,
|
||||
name: ga.title,
|
||||
type: RuleFormType.threshold,
|
||||
dataSourceName: ga.data[0]?.model.datasource,
|
||||
evaluateFor: rule.for,
|
||||
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
||||
noDataState: ga.no_data_state,
|
||||
|
@ -37,8 +37,8 @@ export function isRecordingRulerRule(rule: RulerRuleDTO): rule is RulerRecording
|
||||
return 'record' in rule;
|
||||
}
|
||||
|
||||
export function isGrafanaRulerRule(rule: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
|
||||
return 'grafana_alert' in rule;
|
||||
export function isGrafanaRulerRule(rule?: RulerRuleDTO): rule is RulerGrafanaRuleDTO {
|
||||
return typeof rule === 'object' && 'grafana_alert' in rule;
|
||||
}
|
||||
|
||||
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 { ExpressionQueryEditor } from './ExpressionQueryEditor';
|
||||
import { DataSourceWithBackend } from '@grafana/runtime';
|
||||
@ -29,11 +29,37 @@ export class ExpressionDatasourceApi extends DataSourceWithBackend<ExpressionQue
|
||||
export const ExpressionDatasourceID = '__expr__';
|
||||
export const ExpressionDatasourceUID = '-100';
|
||||
|
||||
export const expressionDatasource = new ExpressionDatasourceApi({
|
||||
export const instanceSettings: DataSourceInstanceSettings = {
|
||||
id: -100,
|
||||
uid: ExpressionDatasourceUID,
|
||||
name: ExpressionDatasourceID,
|
||||
} as DataSourceInstanceSettings);
|
||||
expressionDatasource.meta = {
|
||||
type: 'grafana-expression',
|
||||
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,
|
||||
info: {
|
||||
logos: {
|
||||
@ -42,6 +68,6 @@ expressionDatasource.meta = {
|
||||
},
|
||||
},
|
||||
} as DataSourcePluginMeta;
|
||||
expressionDatasource.components = {
|
||||
dataSource.components = {
|
||||
QueryEditor: ExpressionQueryEditor,
|
||||
};
|
||||
|
@ -13,7 +13,12 @@ import { AppEvents, DataSourceApi, DataSourceInstanceSettings, DataSourceSelectI
|
||||
import { auto } from 'angular';
|
||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||
// 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 { cloneDeep } from 'lodash';
|
||||
|
||||
@ -52,6 +57,10 @@ export class DatasourceSrv implements DataSourceService {
|
||||
return this.settingsMapByName[this.defaultName];
|
||||
}
|
||||
|
||||
if (nameOrUid === ExpressionDatasourceID || nameOrUid === ExpressionDatasourceUID) {
|
||||
return expressionInstanceSettings;
|
||||
}
|
||||
|
||||
// Complex logic to support template variable data source names
|
||||
// For this we just pick the current or first data source in the variable
|
||||
if (nameOrUid[0] === '$') {
|
||||
@ -112,7 +121,7 @@ export class DatasourceSrv implements DataSourceService {
|
||||
|
||||
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
|
||||
// Expression Datasource (not a real datasource)
|
||||
if (name === expressionDatasource.name) {
|
||||
if (name === ExpressionDatasourceID || name === ExpressionDatasourceUID) {
|
||||
this.datasources[name] = expressionDatasource as any;
|
||||
return Promise.resolve(expressionDatasource);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
TimeRange,
|
||||
toLegacyResponseData,
|
||||
} from '@grafana/data';
|
||||
import { QueryEditorRowTitle } from './QueryEditorRowTitle';
|
||||
import { QueryEditorRowHeader } from './QueryEditorRowHeader';
|
||||
import {
|
||||
QueryOperationRow,
|
||||
QueryOperationRowRenderProps,
|
||||
@ -33,10 +33,11 @@ interface Props {
|
||||
data: PanelData;
|
||||
query: DataQuery;
|
||||
queries: DataQuery[];
|
||||
dsSettings: DataSourceInstanceSettings;
|
||||
id: string;
|
||||
index: number;
|
||||
timeRange?: TimeRange;
|
||||
dataSource: DataSourceInstanceSettings;
|
||||
onChangeDataSource?: (dsSettings: DataSourceInstanceSettings) => void;
|
||||
onChangeTimeRange?: (timeRange: TimeRange) => void;
|
||||
onAddQuery: (query: DataQuery) => void;
|
||||
onRemoveQuery: (query: DataQuery) => void;
|
||||
@ -111,7 +112,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
getQueryDataSourceIdentifier(): string | null | undefined {
|
||||
const { query, dsSettings } = this.props;
|
||||
const { query, dataSource: dsSettings } = this.props;
|
||||
return query.datasource ?? dsSettings.name;
|
||||
}
|
||||
|
||||
@ -302,20 +303,18 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
renderTitle = (props: QueryOperationRowRenderProps) => {
|
||||
const { query, dsSettings, onChange, queries, onChangeTimeRange, timeRange } = this.props;
|
||||
const { datasource } = this.state;
|
||||
const isDisabled = query.hide;
|
||||
renderHeader = (props: QueryOperationRowRenderProps) => {
|
||||
const { query, dataSource, onChangeDataSource, onChange, queries, onChangeTimeRange, timeRange } = this.props;
|
||||
|
||||
return (
|
||||
<QueryEditorRowTitle
|
||||
<QueryEditorRowHeader
|
||||
query={query}
|
||||
queries={queries}
|
||||
onTimeRangeChange={onChangeTimeRange}
|
||||
onChangeTimeRange={onChangeTimeRange}
|
||||
timeRange={timeRange}
|
||||
inMixedMode={dsSettings.meta.mixed}
|
||||
dataSourceName={datasource!.name}
|
||||
disabled={isDisabled}
|
||||
onChangeDataSource={onChangeDataSource}
|
||||
dataSource={dataSource}
|
||||
disabled={query.hide}
|
||||
onClick={(e) => this.onToggleEditMode(e, props)}
|
||||
onChange={onChange}
|
||||
collapsedText={!props.isOpen ? this.renderCollapsedText() : null}
|
||||
@ -346,7 +345,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
id={id}
|
||||
draggable={true}
|
||||
index={index}
|
||||
headerElement={this.renderTitle}
|
||||
headerElement={this.renderHeader}
|
||||
actions={this.renderActions}
|
||||
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 { DataQuery, DataSourceInstanceSettings, GrafanaTheme, TimeRange } from '@grafana/data';
|
||||
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 { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource';
|
||||
import { ExpressionDatasourceUID } from '../../expressions/ExpressionDatasource';
|
||||
|
||||
export interface Props {
|
||||
query: DataQuery;
|
||||
queries: DataQuery[];
|
||||
dataSourceName: string;
|
||||
inMixedMode?: boolean;
|
||||
disabled?: boolean;
|
||||
timeRange?: TimeRange;
|
||||
onTimeRangeChange?: (timeRange: TimeRange) => void;
|
||||
dataSource: DataSourceInstanceSettings;
|
||||
onChangeDataSource?: (settings: DataSourceInstanceSettings) => void;
|
||||
onChangeTimeRange?: (timeRange: TimeRange) => void;
|
||||
onChange: (query: DataQuery) => void;
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
collapsedText: string | null;
|
||||
}
|
||||
|
||||
export const QueryEditorRowTitle: React.FC<Props> = ({
|
||||
dataSourceName,
|
||||
inMixedMode,
|
||||
disabled,
|
||||
query,
|
||||
queries,
|
||||
onClick,
|
||||
onChange,
|
||||
onTimeRangeChange,
|
||||
timeRange,
|
||||
collapsedText,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getQueryEditorRowTitleStyles(theme);
|
||||
export const QueryEditorRowHeader: React.FC<Props> = (props) => {
|
||||
const { dataSource, onChangeDataSource, disabled, query, queries, onClick, onChange, collapsedText } = props;
|
||||
|
||||
const styles = useStyles(getStyles);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
@ -91,10 +81,6 @@ export const QueryEditorRowTitle: React.FC<Props> = ({
|
||||
event.target.select();
|
||||
};
|
||||
|
||||
const onDataSourceChange = (dataSource: DataSourceInstanceSettings) => {
|
||||
onChange({ ...query, datasource: dataSource.name });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{!isEditing && (
|
||||
@ -126,17 +112,8 @@ export const QueryEditorRowTitle: React.FC<Props> = ({
|
||||
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
|
||||
</>
|
||||
)}
|
||||
{inMixedMode && (
|
||||
<div style={{ display: 'flex', marginLeft: '8px' }}>
|
||||
{query.datasource !== ExpressionDatasourceID && (
|
||||
<>
|
||||
<DataSourcePicker current={dataSourceName} onChange={onDataSourceChange} />
|
||||
{onTimeRangeChange && timeRange && <TimeRangeInput onChange={onTimeRangeChange} value={timeRange} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{dataSourceName && !inMixedMode && <em className={styles.contextInfo}> ({dataSourceName})</em>}
|
||||
<PickerRenderer {...props} />
|
||||
{dataSource && !onChangeDataSource && <em className={styles.contextInfo}> ({dataSource.name})</em>}
|
||||
{disabled && <em className={styles.contextInfo}> Disabled</em>}
|
||||
|
||||
{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 {
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
@ -225,5 +222,9 @@ const getQueryEditorRowTitleStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
color: ${theme.colors.textWeak};
|
||||
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 { QueryEditorRow } from './QueryEditorRow';
|
||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
interface Props {
|
||||
// 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) => {
|
||||
const { queries, onQueriesChange } = this.props;
|
||||
|
||||
@ -67,21 +97,29 @@ export class QueryEditorRows extends PureComponent<Props> {
|
||||
{(provided) => {
|
||||
return (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{queries.map((query, index) => (
|
||||
<QueryEditorRow
|
||||
dsSettings={dsSettings}
|
||||
id={query.refId}
|
||||
index={index}
|
||||
key={query.refId}
|
||||
data={data}
|
||||
query={query}
|
||||
onChange={(query) => this.onChangeQuery(query, index)}
|
||||
onRemoveQuery={this.onRemoveQuery}
|
||||
onAddQuery={this.props.onAddQuery}
|
||||
onRunQuery={this.props.onRunQueries}
|
||||
queries={queries}
|
||||
/>
|
||||
))}
|
||||
{queries.map((query, index) => {
|
||||
const dataSourceSettings = getDataSourceSettings(query, dsSettings);
|
||||
const onChangeDataSourceSettings = dsSettings.meta.mixed
|
||||
? (settings: DataSourceInstanceSettings) => this.onDataSourceChange(settings, index)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<QueryEditorRow
|
||||
id={query.refId}
|
||||
index={index}
|
||||
key={query.refId}
|
||||
data={data}
|
||||
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}
|
||||
</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 { addQuery } from 'app/core/utils/query';
|
||||
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 { PanelQueryRunner } from '../state/PanelQueryRunner';
|
||||
import { QueryGroupOptionsEditor } from './QueryGroupOptions';
|
||||
|
@ -22,7 +22,11 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { toDataQueryError } from '@grafana/runtime';
|
||||
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';
|
||||
|
||||
type MapOfResponsePackets = { [str: string]: DataQueryResponse };
|
||||
@ -175,7 +179,7 @@ export function callQueryMethod(
|
||||
) {
|
||||
// If any query has an expression, use the expression endpoint
|
||||
for (const target of request.targets) {
|
||||
if (target.datasource === ExpressionDatasourceID) {
|
||||
if (target.datasource === ExpressionDatasourceID || target.datasource === ExpressionDatasourceUID) {
|
||||
return expressionDatasource.query(request as DataQueryRequest<ExpressionQuery>);
|
||||
}
|
||||
}
|
||||
|
@ -92,16 +92,12 @@ export enum GrafanaAlertState {
|
||||
KeepLastState = 'KeepLastState',
|
||||
OK = 'OK',
|
||||
}
|
||||
|
||||
export interface GrafanaQueryModel extends DataQuery {
|
||||
datasource: string;
|
||||
datasourceUid: string;
|
||||
}
|
||||
export interface GrafanaQuery {
|
||||
refId: string;
|
||||
queryType: string;
|
||||
relativeTimeRange: RelativeTimeRange;
|
||||
model: GrafanaQueryModel;
|
||||
datasourceUid: string;
|
||||
model: DataQuery;
|
||||
}
|
||||
|
||||
export interface PostableGrafanaRuleDefinition {
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
Labels,
|
||||
Annotations,
|
||||
RulerRuleGroupDTO,
|
||||
GrafanaQueryModel,
|
||||
} from './unified-alerting-dto';
|
||||
|
||||
export type Alert = {
|
||||
@ -82,7 +81,6 @@ export interface CombinedRule {
|
||||
rulerRule?: RulerRuleDTO;
|
||||
group: CombinedRuleGroup;
|
||||
namespace: CombinedRuleNamespace;
|
||||
queries?: GrafanaQueryModel[];
|
||||
}
|
||||
|
||||
export interface CombinedRuleGroup {
|
||||
|
Loading…
Reference in New Issue
Block a user