mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AlertingNG: Test definition (#30886)
* break out new and edit * changed model to match new model in backend * AlertingNG: API modifications (#30683) * Fix API consistency * Change eval alert definition to POST request * Fix eval endpoint to accept custom now parameter * Change JSON input property for create/update endpoints * model adjustments * set mixed datasource, fix put url * update snapshots * run test response through converters * remove edit and add landing page * remove snapshot tests ans snapshots * wrap linkbutton in array * different approaches to massage data * get instead of post * use function to return instances data * hook up test button in view * test endpoint for not saved definitions * function that return query options * Chore: fixes strict error * hide ng alert button * typings * fix setAlertDef error * better message when you have queries but no data * NGAlert: Refactoring that handles cleaning up state (#31087) * Chore: some refactorings of state * Chore: reduces strict null errors Co-authored-by: Sofia Papagiannaki <papagian@users.noreply.github.com> Co-authored-by: Sofia Papagiannaki <sofia@grafana.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
This commit is contained in:
parent
b9f6bd7897
commit
12dcba5d0b
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { AlertRuleList, Props } from './AlertRuleList';
|
||||
import { AlertRuleListUnconnected, Props } from './AlertRuleList';
|
||||
import { AlertRule } from '../../types';
|
||||
import appEvents from '../../core/app_events';
|
||||
import { NavModel } from '@grafana/data';
|
||||
@ -24,15 +24,16 @@ const setup = (propOverrides?: object) => {
|
||||
stateFilter: '',
|
||||
search: '',
|
||||
isLoading: false,
|
||||
ngAlertDefinitions: [],
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
const wrapper = shallow(<AlertRuleList {...props} />);
|
||||
const wrapper = shallow(<AlertRuleListUnconnected {...props} />);
|
||||
|
||||
return {
|
||||
wrapper,
|
||||
instance: wrapper.instance() as AlertRuleList,
|
||||
instance: wrapper.instance() as AlertRuleListUnconnected,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
import AlertRuleItem from './AlertRuleItem';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@ -10,24 +10,37 @@ import { AlertDefinition, AlertRule, CoreEvents, StoreState } from 'app/types';
|
||||
import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions';
|
||||
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
import { NavModel, SelectableValue } from '@grafana/data';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { setSearchQuery } from './state/reducers';
|
||||
import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui';
|
||||
import { AlertDefinitionItem } from './components/AlertDefinitionItem';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
alertRules: Array<AlertRule | AlertDefinition>;
|
||||
updateLocation: typeof updateLocation;
|
||||
getAlertRulesAsync: typeof getAlertRulesAsync;
|
||||
setSearchQuery: typeof setSearchQuery;
|
||||
togglePauseAlertRule: typeof togglePauseAlertRule;
|
||||
stateFilter: string;
|
||||
search: string;
|
||||
isLoading: boolean;
|
||||
function mapStateToProps(state: StoreState) {
|
||||
return {
|
||||
navModel: getNavModel(state.navIndex, 'alert-list'),
|
||||
alertRules: getAlertRuleItems(state),
|
||||
stateFilter: state.location.query.state,
|
||||
search: getSearchQuery(state.alertRules),
|
||||
isLoading: state.alertRules.isLoading,
|
||||
ngAlertDefinitions: state.alertDefinition.alertDefinitions,
|
||||
};
|
||||
}
|
||||
|
||||
export class AlertRuleList extends PureComponent<Props, any> {
|
||||
const mapDispatchToProps = {
|
||||
updateLocation,
|
||||
getAlertRulesAsync,
|
||||
setSearchQuery,
|
||||
togglePauseAlertRule,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
export type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
export class AlertRuleListUnconnected extends PureComponent<Props, any> {
|
||||
stateFilters = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'OK', value: 'ok' },
|
||||
@ -118,9 +131,11 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<LinkButton variant="primary" href="alerting/new">
|
||||
Add NG Alert
|
||||
</LinkButton>
|
||||
{config.featureToggles.ngalert && (
|
||||
<LinkButton variant="primary" href="alerting/new">
|
||||
Add NG Alert
|
||||
</LinkButton>
|
||||
)}
|
||||
<Button variant="secondary" onClick={this.onOpenHowTo}>
|
||||
How to add an alert
|
||||
</Button>
|
||||
@ -153,20 +168,4 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
navModel: getNavModel(state.navIndex, 'alert-list'),
|
||||
alertRules: getAlertRuleItems(state),
|
||||
stateFilter: state.location.query.state,
|
||||
search: getSearchQuery(state.alertRules),
|
||||
isLoading: state.alertRules.isLoading,
|
||||
ngAlertDefinitions: state.alertDefinition.alertDefinitions,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation,
|
||||
getAlertRulesAsync,
|
||||
setSearchQuery,
|
||||
togglePauseAlertRule,
|
||||
};
|
||||
|
||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList));
|
||||
export default hot(module)(connector(AlertRuleListUnconnected));
|
||||
|
@ -1,49 +1,63 @@
|
||||
import React, { FormEvent, PureComponent } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui';
|
||||
|
||||
import { config } from 'app/core/config';
|
||||
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
|
||||
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
|
||||
import AlertingQueryEditor from './components/AlertingQueryEditor';
|
||||
import { AlertingQueryEditor } from './components/AlertingQueryEditor';
|
||||
import { AlertDefinitionOptions } from './components/AlertDefinitionOptions';
|
||||
import { AlertingQueryPreview } from './components/AlertingQueryPreview';
|
||||
import {
|
||||
updateAlertDefinitionOption,
|
||||
cleanUpDefinitionState,
|
||||
createAlertDefinition,
|
||||
updateAlertDefinitionUiState,
|
||||
updateAlertDefinition,
|
||||
evaluateAlertDefinition,
|
||||
evaluateNotSavedAlertDefinition,
|
||||
getAlertDefinition,
|
||||
onRunQueries,
|
||||
updateAlertDefinition,
|
||||
updateAlertDefinitionOption,
|
||||
updateAlertDefinitionUiState,
|
||||
} from './state/actions';
|
||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||
import { AlertDefinition, AlertDefinitionUiState, QueryGroupOptions, StoreState } from '../../types';
|
||||
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
|
||||
import { StoreState } from 'app/types';
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
const pageId = getRouteParamsId(state.location);
|
||||
|
||||
return {
|
||||
uiState: state.alertDefinition.uiState,
|
||||
getQueryOptions: state.alertDefinition.getQueryOptions,
|
||||
queryRunner: state.alertDefinition.queryRunner,
|
||||
getInstances: state.alertDefinition.getInstances,
|
||||
alertDefinition: state.alertDefinition.alertDefinition,
|
||||
pageId: (pageId as string) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateAlertDefinitionUiState,
|
||||
updateAlertDefinitionOption,
|
||||
evaluateAlertDefinition,
|
||||
updateAlertDefinition,
|
||||
createAlertDefinition,
|
||||
getAlertDefinition,
|
||||
evaluateNotSavedAlertDefinition,
|
||||
onRunQueries,
|
||||
cleanUpDefinitionState,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {
|
||||
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition;
|
||||
}
|
||||
|
||||
interface ConnectedProps {
|
||||
uiState: AlertDefinitionUiState;
|
||||
queryRunner: PanelQueryRunner;
|
||||
queryOptions: QueryGroupOptions;
|
||||
alertDefinition: AlertDefinition;
|
||||
pageId: string;
|
||||
}
|
||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
interface DispatchProps {
|
||||
updateAlertDefinitionUiState: typeof updateAlertDefinitionUiState;
|
||||
updateAlertDefinitionOption: typeof updateAlertDefinitionOption;
|
||||
getAlertDefinition: typeof getAlertDefinition;
|
||||
updateAlertDefinition: typeof updateAlertDefinition;
|
||||
createAlertDefinition: typeof createAlertDefinition;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ConnectedProps & DispatchProps;
|
||||
|
||||
class NextGenAlertingPage extends PureComponent<Props> {
|
||||
class NextGenAlertingPageUnconnected extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
const { getAlertDefinition, pageId } = this.props;
|
||||
|
||||
@ -52,8 +66,13 @@ class NextGenAlertingPage extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
onChangeAlertOption = (event: FormEvent<HTMLFormElement>) => {
|
||||
this.props.updateAlertDefinitionOption({ [event.currentTarget.name]: event.currentTarget.value });
|
||||
componentWillUnmount() {
|
||||
this.props.cleanUpDefinitionState();
|
||||
}
|
||||
|
||||
onChangeAlertOption = (event: FormEvent<HTMLElement>) => {
|
||||
const formEvent = event as FormEvent<HTMLFormElement>;
|
||||
this.props.updateAlertDefinitionOption({ [formEvent.currentTarget.name]: formEvent.currentTarget.value });
|
||||
};
|
||||
|
||||
onChangeInterval = (interval: SelectableValue<number>) => {
|
||||
@ -80,7 +99,14 @@ class NextGenAlertingPage extends PureComponent<Props> {
|
||||
|
||||
onDiscard = () => {};
|
||||
|
||||
onTest = () => {};
|
||||
onTest = () => {
|
||||
const { alertDefinition, evaluateAlertDefinition, evaluateNotSavedAlertDefinition } = this.props;
|
||||
if (alertDefinition.uid) {
|
||||
evaluateAlertDefinition();
|
||||
} else {
|
||||
evaluateNotSavedAlertDefinition();
|
||||
}
|
||||
};
|
||||
|
||||
renderToolbarActions() {
|
||||
return [
|
||||
@ -97,8 +123,18 @@ class NextGenAlertingPage extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { alertDefinition, uiState, updateAlertDefinitionUiState, queryRunner, queryOptions } = this.props;
|
||||
const {
|
||||
alertDefinition,
|
||||
getInstances,
|
||||
uiState,
|
||||
updateAlertDefinitionUiState,
|
||||
queryRunner,
|
||||
getQueryOptions,
|
||||
onRunQueries,
|
||||
} = this.props;
|
||||
|
||||
const styles = getStyles(config.theme);
|
||||
const queryOptions = getQueryOptions();
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
@ -108,7 +144,14 @@ class NextGenAlertingPage extends PureComponent<Props> {
|
||||
<div className={styles.splitPanesWrapper}>
|
||||
<SplitPaneWrapper
|
||||
leftPaneComponents={[
|
||||
<AlertingQueryPreview key="queryPreview" queryRunner={queryRunner} />,
|
||||
<AlertingQueryPreview
|
||||
key="queryPreview"
|
||||
queryRunner={queryRunner!} // if the queryRunner is undefined here somethings very wrong so it's ok to throw an unhandled error
|
||||
getInstances={getInstances}
|
||||
queries={queryOptions.queries}
|
||||
onTest={this.onTest}
|
||||
onRunQueries={onRunQueries}
|
||||
/>,
|
||||
<AlertingQueryEditor key="queryEditor" />,
|
||||
]}
|
||||
uiState={uiState}
|
||||
@ -129,29 +172,7 @@ class NextGenAlertingPage extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => {
|
||||
const pageId = getRouteParamsId(state.location);
|
||||
|
||||
return {
|
||||
uiState: state.alertDefinition.uiState,
|
||||
queryOptions: state.alertDefinition.queryOptions,
|
||||
queryRunner: state.alertDefinition.queryRunner,
|
||||
alertDefinition: state.alertDefinition.alertDefinition,
|
||||
pageId: (pageId as string) ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
updateAlertDefinitionUiState,
|
||||
updateAlertDefinitionOption,
|
||||
updateAlertDefinition,
|
||||
createAlertDefinition,
|
||||
getAlertDefinition,
|
||||
};
|
||||
|
||||
export default hot(module)(
|
||||
connectWithCleanUp(mapStateToProps, mapDispatchToProps, (state) => state.alertDefinition)(NextGenAlertingPage)
|
||||
);
|
||||
export default hot(module)(connector(NextGenAlertingPageUnconnected));
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
wrapper: css`
|
||||
|
@ -12,7 +12,7 @@ const intervalOptions: Array<SelectableValue<number>> = [
|
||||
|
||||
interface Props {
|
||||
alertDefinition: AlertDefinition;
|
||||
onChange: (event: FormEvent) => void;
|
||||
onChange: (event: FormEvent<HTMLElement>) => void;
|
||||
onIntervalChange: (interval: SelectableValue<number>) => void;
|
||||
onConditionChange: (refId: SelectableValue<string>) => void;
|
||||
queryOptions: QueryGroupOptions;
|
||||
|
@ -1,28 +1,33 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { RefreshPicker, stylesFactory } from '@grafana/ui';
|
||||
|
||||
import { config } from 'app/core/config';
|
||||
import { QueryGroup } from '../../query/components/QueryGroup';
|
||||
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
|
||||
import { onRunQueries, queryOptionsChange } from '../state/actions';
|
||||
import { QueryGroupOptions, StoreState } from 'app/types';
|
||||
|
||||
function mapStateToProps(state: StoreState) {
|
||||
return {
|
||||
queryOptions: state.alertDefinition.getQueryOptions(),
|
||||
queryRunner: state.alertDefinition.queryRunner,
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
queryOptionsChange,
|
||||
onRunQueries,
|
||||
};
|
||||
|
||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
interface OwnProps {}
|
||||
|
||||
interface ConnectedProps {
|
||||
queryOptions: QueryGroupOptions;
|
||||
queryRunner: PanelQueryRunner;
|
||||
}
|
||||
interface DispatchProps {
|
||||
queryOptionsChange: typeof queryOptionsChange;
|
||||
onRunQueries: typeof onRunQueries;
|
||||
}
|
||||
type Props = OwnProps & ConnectedProps<typeof connector>;
|
||||
|
||||
type Props = ConnectedProps & DispatchProps & OwnProps;
|
||||
|
||||
export class AlertingQueryEditor extends PureComponent<Props> {
|
||||
class AlertingQueryEditorUnconnected extends PureComponent<Props> {
|
||||
onQueryOptionsChange = (queryOptions: QueryGroupOptions) => {
|
||||
this.props.queryOptionsChange(queryOptions);
|
||||
};
|
||||
@ -51,7 +56,7 @@ export class AlertingQueryEditor extends PureComponent<Props> {
|
||||
/>
|
||||
</div>
|
||||
<QueryGroup
|
||||
queryRunner={queryRunner}
|
||||
queryRunner={queryRunner!} // if the queryRunner is undefined here somethings very wrong so it's ok to throw an unhandled error
|
||||
options={queryOptions}
|
||||
onRunQueries={this.onRunQueries}
|
||||
onOptionsChange={this.onQueryOptionsChange}
|
||||
@ -62,19 +67,7 @@ export class AlertingQueryEditor extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => {
|
||||
return {
|
||||
queryOptions: state.alertDefinition.queryOptions,
|
||||
queryRunner: state.alertDefinition.queryRunner,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
|
||||
queryOptionsChange,
|
||||
onRunQueries,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AlertingQueryEditor);
|
||||
export const AlertingQueryEditor = connector(AlertingQueryEditorUnconnected);
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
|
@ -2,8 +2,8 @@ import React, { FC, useMemo, useState } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import { css } from 'emotion';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { TabsBar, TabContent, Tab, useStyles, Icon } from '@grafana/ui';
|
||||
import { DataFrame, DataQuery, GrafanaTheme, PanelData } from '@grafana/data';
|
||||
import { Button, Icon, Tab, TabContent, TabsBar, useStyles } from '@grafana/ui';
|
||||
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
|
||||
import { PreviewQueryTab } from './PreviewQueryTab';
|
||||
import { PreviewInstancesTab } from './PreviewInstancesTab';
|
||||
@ -20,14 +20,19 @@ const tabs = [
|
||||
|
||||
interface Props {
|
||||
queryRunner: PanelQueryRunner;
|
||||
getInstances: () => DataFrame[];
|
||||
queries: DataQuery[];
|
||||
onTest: () => void;
|
||||
onRunQueries: () => void;
|
||||
}
|
||||
|
||||
export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => {
|
||||
export const AlertingQueryPreview: FC<Props> = ({ getInstances, onRunQueries, onTest, queryRunner, queries }) => {
|
||||
const [activeTab, setActiveTab] = useState<string>(Tabs.Query);
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
|
||||
const data = useObservable(observable);
|
||||
const data = useObservable<PanelData>(observable);
|
||||
const instances = getInstances();
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
@ -49,16 +54,33 @@ export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => {
|
||||
<h4 className={styles.noQueriesHeader}>There was an error :(</h4>
|
||||
<div>{data.error?.data?.error}</div>
|
||||
</div>
|
||||
) : data && data.series.length > 0 ? (
|
||||
) : queries && queries.length > 0 ? (
|
||||
<AutoSizer style={{ width: '100%', height: '100%' }}>
|
||||
{({ width, height }) => {
|
||||
switch (activeTab) {
|
||||
case Tabs.Instances:
|
||||
return <PreviewInstancesTab isTested={false} data={data} styles={styles} />;
|
||||
return (
|
||||
<PreviewInstancesTab
|
||||
isTested={instances.length > 0}
|
||||
instances={instances}
|
||||
styles={styles}
|
||||
width={width}
|
||||
height={height}
|
||||
onTest={onTest}
|
||||
/>
|
||||
);
|
||||
|
||||
case Tabs.Query:
|
||||
default:
|
||||
return <PreviewQueryTab data={data} width={width} height={height} />;
|
||||
if (data) {
|
||||
return <PreviewQueryTab data={data} width={width} height={height} />;
|
||||
}
|
||||
return (
|
||||
<div className={styles.noQueries}>
|
||||
<h4 className={styles.noQueriesHeader}>Run queries to view data.</h4>
|
||||
<Button onClick={onRunQueries}>Run queries</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}}
|
||||
</AutoSizer>
|
||||
|
@ -1,23 +1,26 @@
|
||||
import React, { FC } from 'react';
|
||||
import { PanelData } from '@grafana/data';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { Button, Table } from '@grafana/ui';
|
||||
import { PreviewStyles } from './AlertingQueryPreview';
|
||||
|
||||
interface Props {
|
||||
data: PanelData;
|
||||
instances: DataFrame[];
|
||||
isTested: boolean;
|
||||
styles: PreviewStyles;
|
||||
width: number;
|
||||
height: number;
|
||||
onTest: () => void;
|
||||
}
|
||||
|
||||
export const PreviewInstancesTab: FC<Props> = ({ data, isTested, styles }) => {
|
||||
export const PreviewInstancesTab: FC<Props> = ({ instances, isTested, onTest, height, styles, width }) => {
|
||||
if (!isTested) {
|
||||
return (
|
||||
<div className={styles.noQueries}>
|
||||
<h4 className={styles.noQueriesHeader}>You haven’t tested your alert yet.</h4>
|
||||
<div>In order to see your instances, you need to test your alert first.</div>
|
||||
<Button>Test alert now</Button>
|
||||
<Button onClick={onTest}>Test alert now</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <div>Instances</div>;
|
||||
return <Table data={instances[0]} height={height} width={width} />;
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { getFrameDisplayName, GrafanaTheme, PanelData } from '@grafana/data';
|
||||
import { getFrameDisplayName, GrafanaTheme, PanelData, SelectableValue, toDataFrame } from '@grafana/data';
|
||||
import { Select, stylesFactory, Table, useTheme } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
|
||||
interface Props {
|
||||
data: PanelData;
|
||||
data?: PanelData;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
@ -13,14 +13,21 @@ export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => {
|
||||
const [currentSeries, setSeries] = useState<number>(0);
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme, height);
|
||||
const series = useMemo(
|
||||
() => data.series.map((frame, index) => ({ value: index, label: getFrameDisplayName(frame) })),
|
||||
[data.series]
|
||||
);
|
||||
const series = useMemo<Array<SelectableValue<number>>>(() => {
|
||||
if (data?.series) {
|
||||
return data.series.map((frame, index) => ({ value: index, label: getFrameDisplayName(frame) }));
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [data]);
|
||||
|
||||
// Select padding
|
||||
const padding = 16;
|
||||
|
||||
if (!data?.series?.length) {
|
||||
return <Table data={toDataFrame([])} height={height} width={width} />;
|
||||
}
|
||||
|
||||
if (data.series.length > 1) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
|
@ -1,29 +1,38 @@
|
||||
import { AppEvents, dateMath } from '@grafana/data';
|
||||
import {
|
||||
AppEvents,
|
||||
applyFieldOverrides,
|
||||
arrowTableToDataFrame,
|
||||
base64StringToArrowTable,
|
||||
DataSourceApi,
|
||||
dateMath,
|
||||
} from '@grafana/data';
|
||||
import { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
import store from 'app/core/store';
|
||||
import {
|
||||
notificationChannelLoaded,
|
||||
ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
|
||||
cleanUpState,
|
||||
loadAlertRules,
|
||||
loadedAlertRules,
|
||||
setNotificationChannels,
|
||||
setUiState,
|
||||
ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
|
||||
updateAlertDefinitionOptions,
|
||||
setQueryOptions,
|
||||
setAlertDefinitions,
|
||||
notificationChannelLoaded,
|
||||
setAlertDefinition,
|
||||
setAlertDefinitions,
|
||||
setInstanceData,
|
||||
setNotificationChannels,
|
||||
setQueryOptions,
|
||||
setUiState,
|
||||
updateAlertDefinitionOptions,
|
||||
} from './reducers';
|
||||
import {
|
||||
AlertDefinition,
|
||||
AlertDefinitionState,
|
||||
AlertDefinitionUiState,
|
||||
AlertRuleDTO,
|
||||
NotifierDTO,
|
||||
ThunkResult,
|
||||
QueryGroupOptions,
|
||||
QueryGroupDataSource,
|
||||
AlertDefinitionState,
|
||||
QueryGroupOptions,
|
||||
ThunkResult,
|
||||
} from 'app/types';
|
||||
import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource';
|
||||
import { ExpressionQuery } from '../../expressions/types';
|
||||
@ -161,10 +170,12 @@ export function queryOptionsChange(queryOptions: QueryGroupOptions): ThunkResult
|
||||
|
||||
export function onRunQueries(): ThunkResult<void> {
|
||||
return (dispatch, getStore) => {
|
||||
const { queryRunner, queryOptions } = getStore().alertDefinition;
|
||||
const { queryRunner, getQueryOptions } = getStore().alertDefinition;
|
||||
const timeRange = { from: 'now-1h', to: 'now' };
|
||||
const queryOptions = getQueryOptions();
|
||||
|
||||
queryRunner.run({
|
||||
queryRunner!.run({
|
||||
// if the queryRunner is undefined here somethings very wrong so it's ok to throw an unhandled error
|
||||
timezone: 'browser',
|
||||
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
|
||||
maxDataPoints: queryOptions.maxDataPoints ?? 100,
|
||||
@ -175,41 +186,99 @@ export function onRunQueries(): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function evaluateAlertDefinition(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const { alertDefinition } = getStore().alertDefinition;
|
||||
|
||||
const response: { instances: string[] } = await getBackendSrv().get(
|
||||
`/api/alert-definitions/eval/${alertDefinition.uid}`
|
||||
);
|
||||
|
||||
const handledResponse = handleBase64Response(response.instances);
|
||||
|
||||
dispatch(setInstanceData(handledResponse));
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Alert definition tested successfully']);
|
||||
};
|
||||
}
|
||||
|
||||
export function evaluateNotSavedAlertDefinition(): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
const { alertDefinition, getQueryOptions } = getStore().alertDefinition;
|
||||
const defaultDataSource = await getDataSourceSrv().get(null);
|
||||
|
||||
const response: { instances: string[] } = await getBackendSrv().post('/api/alert-definitions/eval', {
|
||||
condition: alertDefinition.condition,
|
||||
data: buildDataQueryModel(getQueryOptions(), defaultDataSource),
|
||||
});
|
||||
|
||||
const handledResponse = handleBase64Response(response.instances);
|
||||
dispatch(setInstanceData(handledResponse));
|
||||
appEvents.emit(AppEvents.alertSuccess, ['Alert definition tested successfully']);
|
||||
};
|
||||
}
|
||||
|
||||
export function cleanUpDefinitionState(): ThunkResult<void> {
|
||||
return (dispatch) => {
|
||||
dispatch(cleanUpState(undefined));
|
||||
};
|
||||
}
|
||||
|
||||
async function buildAlertDefinition(state: AlertDefinitionState) {
|
||||
const queryOptions = state.queryOptions;
|
||||
const queryOptions = state.getQueryOptions();
|
||||
const currentAlertDefinition = state.alertDefinition;
|
||||
const defaultDataSource = await getDataSourceSrv().get(null);
|
||||
|
||||
return {
|
||||
...currentAlertDefinition,
|
||||
data: queryOptions.queries.map((query) => {
|
||||
let dataSource: QueryGroupDataSource;
|
||||
const isExpression = query.datasource === ExpressionDatasourceID;
|
||||
|
||||
if (isExpression) {
|
||||
dataSource = { name: ExpressionDatasourceID, uid: ExpressionDatasourceID };
|
||||
} else {
|
||||
const dataSourceSetting = getDataSourceSrv().getInstanceSettings(query.datasource);
|
||||
|
||||
dataSource = {
|
||||
name: dataSourceSetting?.name ?? defaultDataSource.name,
|
||||
uid: dataSourceSetting?.uid ?? defaultDataSource.uid,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
model: {
|
||||
...query,
|
||||
type: isExpression ? (query as ExpressionQuery).type : query.queryType,
|
||||
datasource: dataSource.name,
|
||||
datasourceUid: dataSource.uid,
|
||||
},
|
||||
refId: query.refId,
|
||||
relativeTimeRange: {
|
||||
From: 500,
|
||||
To: 0,
|
||||
},
|
||||
};
|
||||
}),
|
||||
data: buildDataQueryModel(queryOptions, defaultDataSource),
|
||||
};
|
||||
}
|
||||
|
||||
function handleBase64Response(frames: string[]) {
|
||||
const dataFrames = frames.map((instance) => {
|
||||
const table = base64StringToArrowTable(instance);
|
||||
return arrowTableToDataFrame(table);
|
||||
});
|
||||
|
||||
return applyFieldOverrides({
|
||||
data: dataFrames,
|
||||
fieldConfig: {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
},
|
||||
replaceVariables: (value: any) => value,
|
||||
theme: config.theme,
|
||||
});
|
||||
}
|
||||
|
||||
function buildDataQueryModel(queryOptions: QueryGroupOptions, defaultDataSource: DataSourceApi) {
|
||||
return queryOptions.queries.map((query) => {
|
||||
let dataSource: QueryGroupDataSource;
|
||||
const isExpression = query.datasource === ExpressionDatasourceID;
|
||||
|
||||
if (isExpression) {
|
||||
dataSource = { name: ExpressionDatasourceID, uid: ExpressionDatasourceID };
|
||||
} else {
|
||||
const dataSourceSetting = getDataSourceSrv().getInstanceSettings(query.datasource);
|
||||
|
||||
dataSource = {
|
||||
name: dataSourceSetting?.name ?? defaultDataSource.name,
|
||||
uid: dataSourceSetting?.uid ?? defaultDataSource.uid,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
model: {
|
||||
...query,
|
||||
type: isExpression ? (query as ExpressionQuery).type : query.queryType,
|
||||
datasource: dataSource.name,
|
||||
datasourceUid: dataSource.uid,
|
||||
},
|
||||
refId: query.refId,
|
||||
relativeTimeRange: {
|
||||
From: 500,
|
||||
To: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { ApplyFieldOverrideOptions, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data';
|
||||
import { ApplyFieldOverrideOptions, DataFrame, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data';
|
||||
import alertDef from './alertDef';
|
||||
import {
|
||||
AlertDefinition,
|
||||
@ -62,11 +62,14 @@ export const initialAlertDefinitionState: AlertDefinitionState = {
|
||||
data: [],
|
||||
intervalSeconds: 60,
|
||||
},
|
||||
queryOptions: { maxDataPoints: 100, dataSource: { name: '-- Mixed --' }, queries: [] },
|
||||
queryRunner: new PanelQueryRunner(dataConfig),
|
||||
uiState: { ...store.getObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, DEFAULT_ALERT_DEFINITION_UI_STATE) },
|
||||
data: [],
|
||||
alertDefinitions: [] as AlertDefinition[],
|
||||
/* These are functions as they are mutated later on and redux toolkit will Object.freeze state so
|
||||
* we need to store these using functions instead */
|
||||
getInstances: () => [] as DataFrame[],
|
||||
getQueryOptions: () => ({ maxDataPoints: 100, dataSource: { name: '-- Mixed --' }, queries: [] }),
|
||||
};
|
||||
|
||||
function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
|
||||
@ -160,37 +163,49 @@ const alertDefinitionSlice = createSlice({
|
||||
initialState: initialAlertDefinitionState,
|
||||
reducers: {
|
||||
setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionDTO>) => {
|
||||
return {
|
||||
...state,
|
||||
alertDefinition: {
|
||||
title: action.payload.title,
|
||||
id: action.payload.id,
|
||||
uid: action.payload.uid,
|
||||
condition: action.payload.condition,
|
||||
intervalSeconds: action.payload.intervalSeconds,
|
||||
data: action.payload.data,
|
||||
description: '',
|
||||
},
|
||||
queryOptions: {
|
||||
...state.queryOptions,
|
||||
queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })),
|
||||
},
|
||||
};
|
||||
const currentOptions = state.getQueryOptions();
|
||||
|
||||
state.alertDefinition.title = action.payload.title;
|
||||
state.alertDefinition.id = action.payload.id;
|
||||
state.alertDefinition.uid = action.payload.uid;
|
||||
state.alertDefinition.condition = action.payload.condition;
|
||||
state.alertDefinition.intervalSeconds = action.payload.intervalSeconds;
|
||||
state.alertDefinition.data = action.payload.data;
|
||||
state.alertDefinition.description = action.payload.description;
|
||||
state.getQueryOptions = () => ({
|
||||
...currentOptions,
|
||||
queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })),
|
||||
});
|
||||
},
|
||||
updateAlertDefinitionOptions: (state: AlertDefinitionState, action: PayloadAction<Partial<AlertDefinition>>) => {
|
||||
return { ...state, alertDefinition: { ...state.alertDefinition, ...action.payload } };
|
||||
state.alertDefinition = { ...state.alertDefinition, ...action.payload };
|
||||
},
|
||||
setUiState: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionUiState>) => {
|
||||
return { ...state, uiState: { ...state.uiState, ...action.payload } };
|
||||
state.uiState = { ...state.uiState, ...action.payload };
|
||||
},
|
||||
setQueryOptions: (state: AlertDefinitionState, action: PayloadAction<QueryGroupOptions>) => {
|
||||
return {
|
||||
...state,
|
||||
queryOptions: action.payload,
|
||||
};
|
||||
state.getQueryOptions = () => action.payload;
|
||||
},
|
||||
setAlertDefinitions: (state: AlertDefinitionState, action: PayloadAction<AlertDefinition[]>) => {
|
||||
return { ...state, alertDefinitions: action.payload };
|
||||
state.alertDefinitions = action.payload;
|
||||
},
|
||||
setInstanceData: (state: AlertDefinitionState, action: PayloadAction<DataFrame[]>) => {
|
||||
state.getInstances = () => action.payload;
|
||||
},
|
||||
cleanUpState: (state: AlertDefinitionState, action: PayloadAction<undefined>) => {
|
||||
if (state.queryRunner) {
|
||||
state.queryRunner.destroy();
|
||||
state.queryRunner = undefined;
|
||||
delete state.queryRunner;
|
||||
state.queryRunner = new PanelQueryRunner(dataConfig);
|
||||
}
|
||||
|
||||
state.alertDefinitions = initialAlertDefinitionState.alertDefinitions;
|
||||
state.alertDefinition = initialAlertDefinitionState.alertDefinition;
|
||||
state.data = initialAlertDefinitionState.data;
|
||||
state.getInstances = initialAlertDefinitionState.getInstances;
|
||||
state.getQueryOptions = initialAlertDefinitionState.getQueryOptions;
|
||||
state.uiState = initialAlertDefinitionState.uiState;
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -209,6 +224,8 @@ export const {
|
||||
setQueryOptions,
|
||||
setAlertDefinitions,
|
||||
setAlertDefinition,
|
||||
setInstanceData,
|
||||
cleanUpState,
|
||||
} = alertDefinitionSlice.actions;
|
||||
|
||||
export const alertRulesReducer = alertRulesSlice.reducer;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataQuery, PanelData, SelectableValue, TimeRange } from '@grafana/data';
|
||||
import { DataFrame, DataQuery, PanelData, SelectableValue, TimeRange } from '@grafana/data';
|
||||
import { PanelQueryRunner } from '../features/query/state/PanelQueryRunner';
|
||||
import { QueryGroupOptions } from './query';
|
||||
import { ExpressionQuery } from '../features/expressions/types';
|
||||
@ -140,10 +140,11 @@ export interface AlertNotification {
|
||||
export interface AlertDefinitionState {
|
||||
uiState: AlertDefinitionUiState;
|
||||
alertDefinition: AlertDefinition;
|
||||
queryOptions: QueryGroupOptions;
|
||||
queryRunner: PanelQueryRunner;
|
||||
queryRunner?: PanelQueryRunner;
|
||||
data: PanelData[];
|
||||
alertDefinitions: AlertDefinition[];
|
||||
getInstances: () => DataFrame[];
|
||||
getQueryOptions: () => QueryGroupOptions;
|
||||
}
|
||||
|
||||
export interface AlertDefinition {
|
||||
|
Loading…
Reference in New Issue
Block a user