grafana/public/app/features/alerting/components/AlertingQueryEditor.tsx
Marcus Andersson 2aa1a051f0
Alerting: visualizing data when creating/editing alerting rule. (#34191)
* VizWrapper to handle some state

* alertingqueryrunner first edition

* added so we always set name and uid when changing datasource.

* wip.

* wip

* added support for canceling requests.

* util for getting time ranges for expression queries

* remove logs, store data in state

* merge from run branch

* incremental commit

* viz working

* added structure for marble testing.

* paddings and move viz picker

* less height for viz, less width on rows

* change so the expression buttons doesnt submit form.

* fixed run button.

* replaced mocks with implementation that will set default query + expression.

* merge with run queries

* fixed so we set a datasource name for the default expression rule.

* improving expression guard.

* lots of styling fixes for viz

* adding placeholder for relative time range.

* fixed story.

* added basic structure to handle open/close of time range picker.

* removed section from TimeOptions since it isn't used any where.

* adding mapper and tests

* move relativetimepicker to its own dir

* added some simple tests.

* changed test.

* use relativetimerangeinput

* redo state management

* refactored the tests.

* replace timerange with relativetimerange

* wip

* wip

* did some refactoring.

* refactored time option formatting.

* added proper formatting and display of time range.

* add relative time description, slight refactor of height

* fixed incorrect import.

* added validator and changed formatting.

* removed unused dep.

* reverted back to internal function.

* fixed display of relative time range picker.

* fixed failing tests.

* fixed parsing issue.

* fixed position of time range picker.

* some more refactorings.

* fixed validation of really big values.

* added another test.

* restored the step2 check.

* fixed merge issue.

* Update packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>

* reverted change.

* fixed merge conflict.

* fixed todo.

* sort some paddings

* replace theme with theme2

Co-authored-by: Peter Holmberg <peter.hlmbrg@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
2021-05-18 16:16:26 +02:00

224 lines
5.9 KiB
TypeScript

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 { AlertingQueryRows } from './AlertingQueryRows';
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 } 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 AlertingQueryEditor 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}>
<AlertingQueryRows
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)};
`,
};
});