mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Move query components to unified folder (#34587)
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
DataQuery,
|
||||
getDefaultRelativeTimeRange,
|
||||
GrafanaTheme2,
|
||||
LoadingState,
|
||||
PanelData,
|
||||
RelativeTimeRange,
|
||||
} from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, HorizontalGroup, Icon, stylesFactory, Tooltip } from '@grafana/ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { QueryRows } from './QueryRows';
|
||||
import {
|
||||
dataSource as expressionDatasource,
|
||||
ExpressionDatasourceUID,
|
||||
} from 'app/features/expressions/ExpressionDatasource';
|
||||
import { getNextRefIdChar } from 'app/core/utils/query';
|
||||
import { defaultCondition } from 'app/features/expressions/utils/expressionTypes';
|
||||
import { ExpressionQueryType } from 'app/features/expressions/types';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
import { AlertingQueryRunner } from '../../state/AlertingQueryRunner';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { isExpressionQuery } from 'app/features/expressions/guards';
|
||||
|
||||
interface Props {
|
||||
value?: GrafanaQuery[];
|
||||
onChange: (queries: GrafanaQuery[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
panelDataByRefId: Record<string, PanelData>;
|
||||
}
|
||||
export class QueryEditor extends PureComponent<Props, State> {
|
||||
private runner: AlertingQueryRunner;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { panelDataByRefId: {} };
|
||||
this.runner = new AlertingQueryRunner();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.runner.get().subscribe((data) => {
|
||||
this.setState({ panelDataByRefId: data });
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.runner.destroy();
|
||||
}
|
||||
|
||||
onRunQueries = () => {
|
||||
const { value = [] } = this.props;
|
||||
this.runner.run(value);
|
||||
};
|
||||
|
||||
onCancelQueries = () => {
|
||||
this.runner.cancel();
|
||||
};
|
||||
|
||||
onDuplicateQuery = (query: GrafanaQuery) => {
|
||||
const { onChange, value = [] } = this.props;
|
||||
onChange(addQuery(value, query));
|
||||
};
|
||||
|
||||
onNewAlertingQuery = () => {
|
||||
const { onChange, value = [] } = this.props;
|
||||
const defaultDataSource = getDatasourceSrv().getInstanceSettings('default');
|
||||
|
||||
if (!defaultDataSource) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(
|
||||
addQuery(value, {
|
||||
datasourceUid: defaultDataSource.uid,
|
||||
model: {
|
||||
refId: '',
|
||||
datasource: defaultDataSource.name,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onNewExpressionQuery = () => {
|
||||
const { onChange, value = [] } = this.props;
|
||||
|
||||
onChange(
|
||||
addQuery(value, {
|
||||
datasourceUid: ExpressionDatasourceUID,
|
||||
model: expressionDatasource.newQuery({
|
||||
type: ExpressionQueryType.classic,
|
||||
conditions: [defaultCondition],
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
renderAddQueryRow(styles: ReturnType<typeof getStyles>) {
|
||||
return (
|
||||
<HorizontalGroup spacing="md" align="flex-start">
|
||||
<Button
|
||||
type="button"
|
||||
icon="plus"
|
||||
onClick={this.onNewAlertingQuery}
|
||||
variant="secondary"
|
||||
aria-label={selectors.components.QueryTab.addQuery}
|
||||
>
|
||||
Query
|
||||
</Button>
|
||||
{config.expressionsEnabled && (
|
||||
<Tooltip content="Experimental feature: queries could stop working in next version" placement="right">
|
||||
<Button
|
||||
type="button"
|
||||
icon="plus"
|
||||
onClick={this.onNewExpressionQuery}
|
||||
variant="secondary"
|
||||
className={styles.expressionButton}
|
||||
>
|
||||
<span>Expression </span>
|
||||
<Icon name="exclamation-triangle" className="muted" size="sm" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
const data = Object.values(this.state.panelDataByRefId).find((d) => Boolean(d));
|
||||
return data?.state === LoadingState.Loading;
|
||||
}
|
||||
|
||||
renderRunQueryButton() {
|
||||
const isRunning = this.isRunning();
|
||||
const styles = getStyles(config.theme2);
|
||||
|
||||
if (isRunning) {
|
||||
return (
|
||||
<div className={styles.runWrapper}>
|
||||
<Button icon="fa fa-spinner" type="button" variant="destructive" onClick={this.onCancelQueries}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.runWrapper}>
|
||||
<Button icon="sync" type="button" onClick={this.onRunQueries}>
|
||||
Run queries
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value = [] } = this.props;
|
||||
const { panelDataByRefId } = this.state;
|
||||
const styles = getStyles(config.theme2);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<QueryRows
|
||||
data={panelDataByRefId}
|
||||
queries={value}
|
||||
onQueriesChange={this.props.onChange}
|
||||
onDuplicateQuery={this.onDuplicateQuery}
|
||||
onRunQueries={this.onRunQueries}
|
||||
/>
|
||||
{this.renderAddQueryRow(styles)}
|
||||
{this.renderRunQueryButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const addQuery = (
|
||||
queries: GrafanaQuery[],
|
||||
queryToAdd: Pick<GrafanaQuery, 'model' | 'datasourceUid'>
|
||||
): GrafanaQuery[] => {
|
||||
const refId = getNextRefIdChar(queries);
|
||||
|
||||
const query: GrafanaQuery = {
|
||||
...queryToAdd,
|
||||
refId,
|
||||
queryType: '',
|
||||
model: {
|
||||
...queryToAdd.model,
|
||||
hide: false,
|
||||
refId,
|
||||
},
|
||||
relativeTimeRange: defaultTimeRange(queryToAdd.model),
|
||||
};
|
||||
|
||||
return [...queries, query];
|
||||
};
|
||||
|
||||
const defaultTimeRange = (model: DataQuery): RelativeTimeRange | undefined => {
|
||||
if (isExpressionQuery(model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getDefaultRelativeTimeRange();
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
container: css`
|
||||
background-color: ${theme.colors.background.primary};
|
||||
height: 100%;
|
||||
`,
|
||||
runWrapper: css`
|
||||
margin-top: ${theme.spacing(1)};
|
||||
`,
|
||||
editorWrapper: css`
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
`,
|
||||
expressionButton: css`
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
`,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import { DataQuery, DataSourceInstanceSettings, PanelData, RelativeTimeRange } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { QueryWrapper } from './QueryWrapper';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
|
||||
interface Props {
|
||||
// The query configuration
|
||||
queries: GrafanaQuery[];
|
||||
data: Record<string, PanelData>;
|
||||
|
||||
// Query editing
|
||||
onQueriesChange: (queries: GrafanaQuery[]) => void;
|
||||
onDuplicateQuery: (query: GrafanaQuery) => void;
|
||||
onRunQueries: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
dataPerQuery: Record<string, PanelData>;
|
||||
}
|
||||
|
||||
export class QueryRows extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = { dataPerQuery: {} };
|
||||
}
|
||||
|
||||
onRemoveQuery = (query: DataQuery) => {
|
||||
this.props.onQueriesChange(
|
||||
this.props.queries.filter((item) => {
|
||||
return item.model.refId !== query.refId;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onChangeTimeRange = (timeRange: RelativeTimeRange, index: number) => {
|
||||
const { queries, onQueriesChange } = this.props;
|
||||
onQueriesChange(
|
||||
queries.map((item, itemIndex) => {
|
||||
if (itemIndex !== index) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
relativeTimeRange: 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 },
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onChangeQuery = (query: DataQuery, index: number) => {
|
||||
const { queries, onQueriesChange } = this.props;
|
||||
|
||||
onQueriesChange(
|
||||
queries.map((item, itemIndex) => {
|
||||
if (itemIndex !== index) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
refId: query.refId,
|
||||
model: {
|
||||
...item.model,
|
||||
...query,
|
||||
datasource: query.datasource!,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onDragEnd = (result: DropResult) => {
|
||||
const { queries, onQueriesChange } = this.props;
|
||||
|
||||
if (!result || !result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = result.source.index;
|
||||
const endIndex = result.destination.index;
|
||||
if (startIndex === endIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const update = Array.from(queries);
|
||||
const [removed] = update.splice(startIndex, 1);
|
||||
update.splice(endIndex, 0, removed);
|
||||
onQueriesChange(update);
|
||||
};
|
||||
|
||||
onDuplicateQuery = (query: DataQuery, source: GrafanaQuery): void => {
|
||||
this.props.onDuplicateQuery({
|
||||
...source,
|
||||
model: query,
|
||||
});
|
||||
};
|
||||
|
||||
getDataSourceSettings = (query: GrafanaQuery): DataSourceInstanceSettings | undefined => {
|
||||
return getDataSourceSrv().getInstanceSettings(query.datasourceUid);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onDuplicateQuery, onRunQueries, queries } = this.props;
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={this.onDragEnd}>
|
||||
<Droppable droppableId="alerting-queries" direction="vertical">
|
||||
{(provided) => {
|
||||
return (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{queries.map((query, index) => {
|
||||
const data = this.props.data ? this.props.data[query.refId] : ({} as PanelData);
|
||||
const dsSettings = this.getDataSourceSettings(query);
|
||||
|
||||
if (!dsSettings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryWrapper
|
||||
index={index}
|
||||
key={`${query.refId}-${index}`}
|
||||
dsSettings={dsSettings}
|
||||
data={data}
|
||||
query={query}
|
||||
onChangeQuery={this.onChangeQuery}
|
||||
onRemoveQuery={this.onRemoveQuery}
|
||||
queries={queries}
|
||||
onChangeDataSource={this.onChangeDataSource}
|
||||
onDuplicateQuery={onDuplicateQuery}
|
||||
onRunQueries={onRunQueries}
|
||||
onChangeTimeRange={this.onChangeTimeRange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Field, InputControl } from '@grafana/ui';
|
||||
import { ExpressionEditor } from './ExpressionEditor';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { AlertingQueryEditor } from '../../../components/AlertingQueryEditor';
|
||||
import { QueryEditor } from './QueryEditor';
|
||||
|
||||
export const QueryStep: FC = () => {
|
||||
const {
|
||||
@@ -35,7 +35,7 @@ export const QueryStep: FC = () => {
|
||||
>
|
||||
<InputControl
|
||||
name="queries"
|
||||
render={({ field: { ref, ...field } }) => <AlertingQueryEditor {...field} />}
|
||||
render={({ field: { ref, ...field } }) => <QueryEditor {...field} />}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (queries) => Array.isArray(queries) && !!queries.length,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { FC, ReactNode } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import {
|
||||
DataQuery,
|
||||
DataSourceInstanceSettings,
|
||||
GrafanaTheme2,
|
||||
PanelData,
|
||||
RelativeTimeRange,
|
||||
getDefaultRelativeTimeRange,
|
||||
} from '@grafana/data';
|
||||
import { useStyles2, RelativeTimeRangePicker } from '@grafana/ui';
|
||||
import { QueryEditorRow } from '../../../../query/components/QueryEditorRow';
|
||||
import { VizWrapper } from './VizWrapper';
|
||||
import { isExpressionQuery } from '../../../../expressions/guards';
|
||||
import { GrafanaQuery } from 'app/types/unified-alerting-dto';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
interface Props {
|
||||
data: PanelData;
|
||||
query: GrafanaQuery;
|
||||
queries: GrafanaQuery[];
|
||||
dsSettings: DataSourceInstanceSettings;
|
||||
onChangeDataSource: (settings: DataSourceInstanceSettings, index: number) => void;
|
||||
onChangeQuery: (query: DataQuery, index: number) => void;
|
||||
onChangeTimeRange?: (timeRange: RelativeTimeRange, index: number) => void;
|
||||
onRemoveQuery: (query: DataQuery) => void;
|
||||
onDuplicateQuery: (query: GrafanaQuery) => void;
|
||||
onRunQueries: () => void;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const QueryWrapper: FC<Props> = ({
|
||||
data,
|
||||
dsSettings,
|
||||
index,
|
||||
onChangeDataSource,
|
||||
onChangeQuery,
|
||||
onChangeTimeRange,
|
||||
onRunQueries,
|
||||
onRemoveQuery,
|
||||
onDuplicateQuery,
|
||||
query,
|
||||
queries,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isExpression = isExpressionQuery(query.model);
|
||||
|
||||
const renderTimePicker = (query: GrafanaQuery, index: number): ReactNode => {
|
||||
if (isExpressionQuery(query.model) || !onChangeTimeRange) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RelativeTimeRangePicker
|
||||
timeRange={query.relativeTimeRange ?? getDefaultRelativeTimeRange()}
|
||||
onChange={(range) => onChangeTimeRange(range, index)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<QueryEditorRow
|
||||
dataSource={dsSettings}
|
||||
onChangeDataSource={!isExpression ? (settings) => onChangeDataSource(settings, index) : undefined}
|
||||
id={query.refId}
|
||||
index={index}
|
||||
key={query.refId}
|
||||
data={data}
|
||||
query={cloneDeep(query.model)}
|
||||
onChange={(query) => onChangeQuery(query, index)}
|
||||
onRemoveQuery={onRemoveQuery}
|
||||
onAddQuery={onDuplicateQuery}
|
||||
onRunQuery={onRunQueries}
|
||||
queries={queries}
|
||||
renderHeaderExtras={() => renderTimePicker(query, index)}
|
||||
/>
|
||||
{data && <VizWrapper data={data} defaultPanel={isExpression ? 'table' : 'timeseries'} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
margin-bottom: ${theme.spacing(1)};
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
border-radius: ${theme.shape.borderRadius(1)};
|
||||
padding-bottom: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
Reference in New Issue
Block a user