Alerting: Move query components to unified folder (#34587)

This commit is contained in:
Peter Holmberg
2021-05-25 11:34:19 +02:00
committed by GitHub
parent 91657dad18
commit dcef87fd79
6 changed files with 20 additions and 17 deletions

View File

@@ -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&nbsp;</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)};
`,
};
});

View File

@@ -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>
);
}
}

View File

@@ -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,

View File

@@ -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)};
`,
});