NgAlerting: View query result (#30218)

* Fix query preview add tabs to options

* break out tabs to components

* add refresh button

* minor things after PR review

* hide queries

* Add simple error screen if there's an error

* dropdown with different frames

* move onrunqueries to redux

* cleanup

* show actual error
This commit is contained in:
Peter Holmberg
2021-01-19 14:04:54 +01:00
committed by GitHub
parent 506120bb81
commit 4b888d105d
16 changed files with 251 additions and 86 deletions

View File

@ -16,6 +16,7 @@ export const getNextRefIdChar = (queries: DataQuery[]): string => {
export function addQuery(queries: DataQuery[], query?: Partial<DataQuery>): DataQuery[] {
const q = query || {};
q.refId = getNextRefIdChar(queries);
q.hide = false;
return [...queries, q as DataQuery];
}

View File

@ -1,9 +1,8 @@
import React, { FC, FormEvent } from 'react';
import React, { FC, FormEvent, useState } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { Field, Input, Select, TextArea, useStyles } from '@grafana/ui';
import { Field, Input, Tab, TabContent, TabsBar, TextArea, useStyles } from '@grafana/ui';
import { AlertDefinition, NotificationChannelType } from 'app/types';
import { mapChannelsToSelectableValue } from '../utils/notificationChannels';
interface Props {
alertDefinition: AlertDefinition;
@ -11,45 +10,70 @@ interface Props {
onChange: (event: FormEvent) => void;
}
export const AlertDefinitionOptions: FC<Props> = ({ alertDefinition, notificationChannelTypes, onChange }) => {
enum Tabs {
Alert = 'alert',
Panel = 'panel',
}
const tabs = [
{ id: Tabs.Alert, text: 'Alert definition' },
{ id: Tabs.Panel, text: 'Panel' },
];
export const AlertDefinitionOptions: FC<Props> = ({ alertDefinition, onChange }) => {
const styles = useStyles(getStyles);
const [activeTab, setActiveTab] = useState<string>(Tabs.Alert);
return (
<div style={{ paddingTop: '16px' }}>
<div className={styles.container}>
<h4>Alert definition</h4>
<Field label="Name">
<Input width={25} name="name" value={alertDefinition.name} onChange={onChange} />
</Field>
<Field label="Description" description="What does the alert do and why was it created">
<TextArea rows={5} width={25} name="description" value={alertDefinition.description} onChange={onChange} />
</Field>
<Field label="Evaluate">
<span>Every For</span>
</Field>
<Field label="Conditions">
<div></div>
</Field>
{notificationChannelTypes.length > 0 && (
<>
<Field label="Notification channel">
<Select options={mapChannelsToSelectableValue(notificationChannelTypes, false)} onChange={onChange} />
<div className={styles.container}>
<TabsBar>
{tabs.map((tab, index) => (
<Tab
key={`${tab.id}-${index}`}
label={tab.text}
active={tab.id === activeTab}
onChangeTab={() => setActiveTab(tab.id)}
/>
))}
</TabsBar>
<TabContent className={styles.tabContent}>
{activeTab === Tabs.Alert && (
<div>
<Field label="Name">
<Input width={25} name="name" value={alertDefinition.name} onChange={onChange} />
</Field>
</>
<Field label="Description" description="What does the alert do and why was it created">
<TextArea
rows={5}
width={25}
name="description"
value={alertDefinition.description}
onChange={onChange}
/>
</Field>
<Field label="Evaluate">
<span>Every For</span>
</Field>
<Field label="Conditions">
<div></div>
</Field>
</div>
)}
</div>
{activeTab === Tabs.Panel && <div>VizPicker</div>}
</TabContent>
</div>
);
};
const getStyles = (theme: GrafanaTheme) => {
return {
wrapper: css`
padding-top: ${theme.spacing.md};
`,
container: css`
padding: ${theme.spacing.md};
background-color: ${theme.colors.panelBg};
margin-top: ${theme.spacing.md};
height: 100%;
`,
tabContent: css`
background: ${theme.colors.panelBg};
height: 100%;
`,
};
};

View File

@ -1,14 +1,13 @@
import React, { PureComponent } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { css } from 'emotion';
import { dateMath, GrafanaTheme } from '@grafana/data';
import { stylesFactory } from '@grafana/ui';
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 { QueryGroupOptions } from '../../query/components/QueryGroupOptions';
import { queryOptionsChange } from '../state/actions';
import { StoreState } from '../../../types';
import { onRunQueries, queryOptionsChange } from '../state/actions';
import { QueryGroupOptions, StoreState } from 'app/types';
interface OwnProps {}
@ -18,6 +17,7 @@ interface ConnectedProps {
}
interface DispatchProps {
queryOptionsChange: typeof queryOptionsChange;
onRunQueries: typeof onRunQueries;
}
type Props = ConnectedProps & DispatchProps & OwnProps;
@ -28,17 +28,11 @@ export class AlertingQueryEditor extends PureComponent<Props> {
};
onRunQueries = () => {
const { queryRunner, queryOptions } = this.props;
const timeRange = { from: 'now-1h', to: 'now' };
this.props.onRunQueries();
};
queryRunner.run({
timezone: 'browser',
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
maxDataPoints: queryOptions.maxDataPoints ?? 100,
minInterval: queryOptions.minInterval,
queries: queryOptions.queries,
datasource: queryOptions.dataSource.name!,
});
onIntervalChanged = (interval: string) => {
this.props.queryOptionsChange({ ...this.props.queryOptions, minInterval: interval });
};
render() {
@ -49,6 +43,13 @@ export class AlertingQueryEditor extends PureComponent<Props> {
<div className={styles.wrapper}>
<div className={styles.container}>
<h4>Queries</h4>
<div className={styles.refreshWrapper}>
<RefreshPicker
onIntervalChanged={this.onIntervalChanged}
onRefresh={this.onRunQueries}
intervals={['15s', '30s']}
/>
</div>
<QueryGroup
queryRunner={queryRunner}
options={queryOptions}
@ -70,6 +71,7 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = s
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
queryOptionsChange,
onRunQueries,
};
export default connect(mapStateToProps, mapDispatchToProps)(AlertingQueryEditor);
@ -85,6 +87,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
background-color: ${theme.colors.panelBg};
height: 100%;
`,
refreshWrapper: css`
display: flex;
justify-content: flex-end;
`,
editorWrapper: css`
border: 1px solid ${theme.colors.panelBorder};
border-radius: ${theme.border.radius.md};

View File

@ -1,18 +1,21 @@
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, Table } from '@grafana/ui';
import { TabsBar, TabContent, Tab, useStyles, Icon } from '@grafana/ui';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { PreviewQueryTab } from './PreviewQueryTab';
import { PreviewInstancesTab } from './PreviewInstancesTab';
enum Tabs {
Query = 'query',
Instance = 'instance',
Instances = 'instances',
}
const tabs = [
{ id: Tabs.Query, text: 'Query', active: true },
{ id: Tabs.Instance, text: 'Alerting instance', active: false },
{ id: Tabs.Query, text: 'Query result' },
{ id: Tabs.Instances, text: 'Alerting instances' },
];
interface Props {
@ -20,11 +23,12 @@ interface Props {
}
export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => {
const [activeTab, setActiveTab] = useState<string>('query');
const [activeTab, setActiveTab] = useState<string>(Tabs.Query);
const styles = useStyles(getStyles);
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
const data = useObservable(observable);
return (
<div className={styles.wrapper}>
<TabsBar>
@ -40,30 +44,65 @@ export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => {
})}
</TabsBar>
<TabContent className={styles.tabContent}>
{activeTab === Tabs.Query && data && (
<div>
<Table data={data.series[0]} width={1200} height={300} />
{data && data.state === 'Error' ? (
<div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>There was an error :(</h4>
<div>{data.error?.data?.error}</div>
</div>
) : data && data.series.length > 0 ? (
<AutoSizer style={{ width: '100%', height: '100%' }}>
{({ width, height }) => {
switch (activeTab) {
case Tabs.Instances:
return <PreviewInstancesTab isTested={false} data={data} styles={styles} />;
case Tabs.Query:
default:
return <PreviewQueryTab data={data} width={width} height={height} />;
}
}}
</AutoSizer>
) : (
<div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>No queries added.</h4>
<div>Start adding queries to this alert and a visualisation for your queries will appear here.</div>
<div>
Learn more about how to create alert definitions <Icon name="external-link-alt" />
</div>
</div>
)}
{activeTab === Tabs.Instance && <div>Instance something something dark side</div>}
</TabContent>
</div>
);
};
const getStyles = (theme: GrafanaTheme) => {
const tabBarHeight = 42;
return {
wrapper: css`
label: alertDefinitionPreviewTabs;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: ${theme.spacing.md} 0 0 ${theme.spacing.md};
`,
tabContent: css`
background: ${theme.colors.panelBg};
height: calc(100% - ${tabBarHeight}px);
height: 100%;
`,
noQueries: css`
color: ${theme.colors.textSemiWeak};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
`,
noQueriesHeader: css`
color: ${theme.colors.textSemiWeak};
`,
};
};
export type PreviewStyles = ReturnType<typeof getStyles>;

View File

@ -0,0 +1,23 @@
import React, { FC } from 'react';
import { PanelData } from '@grafana/data';
import { Button } from '@grafana/ui';
import { PreviewStyles } from './AlertingQueryPreview';
interface Props {
data: PanelData;
isTested: boolean;
styles: PreviewStyles;
}
export const PreviewInstancesTab: FC<Props> = ({ data, isTested, styles }) => {
if (!isTested) {
return (
<div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>You havent 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>
</div>
);
}
return <div>Instances</div>;
};

View File

@ -0,0 +1,52 @@
import React, { FC, useMemo, useState } from 'react';
import { getFrameDisplayName, GrafanaTheme, PanelData } from '@grafana/data';
import { Select, stylesFactory, Table, useTheme } from '@grafana/ui';
import { css } from 'emotion';
interface Props {
data: PanelData;
width: number;
height: number;
}
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]
);
// Select padding
const padding = 16;
if (data.series.length > 1) {
return (
<div className={styles.wrapper}>
<div style={{ height: height - theme.spacing.formInputHeight - 16 }}>
<Table
data={data.series[currentSeries]}
height={height - theme.spacing.formInputHeight - padding}
width={width}
/>
</div>
<div className={styles.selectWrapper}>
<Select onChange={selectedValue => setSeries(selectedValue.value!)} options={series} value={currentSeries} />
</div>
</div>
);
}
return <Table data={data.series[0]} height={height} width={width} />;
};
const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => {
return {
wrapper: css`
height: ${height}px;
`,
selectWrapper: css`
padding: ${theme.spacing.md};
`,
};
});

View File

@ -1,4 +1,4 @@
import { AppEvents } from '@grafana/data';
import { AppEvents, dateMath } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { appEvents } from 'app/core/core';
import { updateLocation } from 'app/core/actions';
@ -14,7 +14,7 @@ import {
setQueryOptions,
} from './reducers';
import { AlertDefinition, AlertDefinitionUiState, AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
import { QueryGroupOptions } from '../../query/components/QueryGroupOptions';
import { QueryGroupOptions } from 'app/types';
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
return async dispatch => {
@ -138,3 +138,19 @@ export function queryOptionsChange(queryOptions: QueryGroupOptions): ThunkResult
dispatch(setQueryOptions(queryOptions));
};
}
export function onRunQueries(): ThunkResult<void> {
return (dispatch, getStore) => {
const { queryRunner, queryOptions } = getStore().alertDefinition;
const timeRange = { from: 'now-1h', to: 'now' };
queryRunner.run({
timezone: 'browser',
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
maxDataPoints: queryOptions.maxDataPoints ?? 100,
minInterval: queryOptions.minInterval,
queries: queryOptions.queries,
datasource: queryOptions.dataSource.name!,
});
};
}

View File

@ -12,11 +12,11 @@ import {
NotificationChannelOption,
NotificationChannelState,
NotifierDTO,
QueryGroupOptions,
} from 'app/types';
import store from 'app/core/store';
import { config } from '@grafana/runtime';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { QueryGroupOptions } from '../../query/components/QueryGroupOptions';
export const ALERT_DEFINITION_UI_STATE_STORAGE_KEY = 'grafana.alerting.alertDefinition.ui';
const DEFAULT_ALERT_DEFINITION_UI_STATE: AlertDefinitionUiState = { rightPaneSize: 400, topPaneSize: 0.45 };

View File

@ -1,8 +1,8 @@
import React, { PureComponent } from 'react';
import { QueryGroup } from 'app/features/query/components/QueryGroup';
import { QueryGroupOptions } from 'app/features/query/components/QueryGroupOptions';
import { PanelModel } from '../../state';
import { getLocationSrv } from '@grafana/runtime';
import { QueryGroupOptions } from 'app/types';
interface Props {
panel: PanelModel;

View File

@ -221,9 +221,9 @@ export class QueryEditorRow extends PureComponent<Props, State> {
};
onDisableQuery = () => {
this.props.query.hide = !this.props.query.hide;
const { query } = this.props;
this.props.onChange({ ...query, hide: !query.hide });
this.props.onRunQuery();
this.forceUpdate();
};
renderCollapsedText(): string | null {

View File

@ -23,9 +23,10 @@ import { Unsubscribable } from 'rxjs';
import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
import { selectors } from '@grafana/e2e-selectors';
import { PanelQueryRunner } from '../state/PanelQueryRunner';
import { QueryGroupOptions, QueryGroupOptionsEditor } from './QueryGroupOptions';
import { QueryGroupOptionsEditor } from './QueryGroupOptions';
import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
import { css } from 'emotion';
import { QueryGroupOptions } from 'app/types';
interface Props {
queryRunner: PanelQueryRunner;

View File

@ -2,7 +2,7 @@
import React, { PureComponent, ChangeEvent, FocusEvent } from 'react';
// Utils
import { rangeUtil, PanelData, DataSourceApi, DataQuery } from '@grafana/data';
import { rangeUtil, PanelData, DataSourceApi } from '@grafana/data';
// Components
import { Switch, Input, InlineField, InlineFormLabel, stylesFactory } from '@grafana/ui';
@ -11,25 +11,7 @@ import { Switch, Input, InlineField, InlineFormLabel, stylesFactory } from '@gra
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { config } from 'app/core/config';
import { css } from 'emotion';
export interface QueryGroupOptions {
queries: DataQuery[];
dataSource: QueryGroupDataSource;
maxDataPoints?: number | null;
minInterval?: string | null;
cacheTimeout?: string | null;
timeRange?: {
from?: string | null;
shift?: string | null;
hide?: boolean;
};
}
interface QueryGroupDataSource {
name?: string | null;
uid?: string;
default?: boolean;
}
import { QueryGroupOptions } from 'app/types';
interface Props {
options: QueryGroupOptions;

View File

@ -4,8 +4,8 @@ import { config } from 'app/core/config';
import React, { FC, useMemo, useState } from 'react';
import { useObservable } from 'react-use';
import { QueryGroup } from '../query/components/QueryGroup';
import { QueryGroupOptions } from '../query/components/QueryGroupOptions';
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
import { QueryGroupOptions } from 'app/types';
interface State {
queryRunner: PanelQueryRunner;

View File

@ -1,6 +1,6 @@
import { PanelData, SelectableValue } from '@grafana/data';
import { PanelQueryRunner } from '../features/query/state/PanelQueryRunner';
import { QueryGroupOptions } from '../features/query/components/QueryGroupOptions';
import { QueryGroupOptions } from './query';
export interface AlertRuleDTO {
id: number;

View File

@ -15,6 +15,7 @@ export * from './store';
export * from './ldap';
export * from './appEvent';
export * from './angular';
export * from './query';
import * as CoreEvents from './events';
export { CoreEvents };

20
public/app/types/query.ts Normal file
View File

@ -0,0 +1,20 @@
import { DataQuery } from '@grafana/data';
export interface QueryGroupOptions {
queries: DataQuery[];
dataSource: QueryGroupDataSource;
maxDataPoints?: number | null;
minInterval?: string | null;
cacheTimeout?: string | null;
timeRange?: {
from?: string | null;
shift?: string | null;
hide?: boolean;
};
}
interface QueryGroupDataSource {
name?: string | null;
uid?: string;
default?: boolean;
}