AlertingNG: Refactor the query preview component (#31281)

* refactor to purecomponent

* use subscription in component

* correct onRunQueries

* move more things from render function

* fix issue with no queries

* pr feedback

* revert to FC

* redo some code layout, simplify if

* minor fixes after review
This commit is contained in:
Peter Holmberg 2021-03-15 10:17:21 +01:00 committed by GitHub
parent cf929237e1
commit 9ffd88b103
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 85 deletions

View File

@ -3,8 +3,8 @@ import { hot } from 'react-hot-loader';
import { connect, ConnectedProps } from 'react-redux';
import { css } from 'emotion';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui';
import { config } from 'app/core/config';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { AlertingQueryEditor } from './components/AlertingQueryEditor';
@ -97,7 +97,9 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
}
};
onDiscard = () => {};
onDiscard = () => {
locationService.replace(`${config.appSubUrl}/alerting/list`);
};
onTest = () => {
const { alertDefinition, evaluateAlertDefinition, evaluateNotSavedAlertDefinition } = this.props;
@ -125,12 +127,12 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
render() {
const {
alertDefinition,
getInstances,
uiState,
updateAlertDefinitionUiState,
queryRunner,
getQueryOptions,
getInstances,
onRunQueries,
queryRunner,
} = this.props;
const styles = getStyles(config.theme);
@ -146,10 +148,10 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
leftPaneComponents={[
<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}
queries={queryOptions.queries}
getInstances={getInstances}
queryRunner={queryRunner!}
onRunQueries={onRunQueries}
/>,
<AlertingQueryEditor key="queryEditor" />,

View File

@ -3,10 +3,12 @@ import { useObservable } from 'react-use';
import { css } from 'emotion';
import AutoSizer from 'react-virtualized-auto-sizer';
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 { config } from '@grafana/runtime';
import { Icon, Tab, TabContent, TabsBar } from '@grafana/ui';
import { PreviewQueryTab } from './PreviewQueryTab';
import { PreviewInstancesTab } from './PreviewInstancesTab';
import { EmptyState } from './EmptyState';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
enum Tabs {
Query = 'query',
@ -26,10 +28,9 @@ interface Props {
onRunQueries: () => void;
}
export const AlertingQueryPreview: FC<Props> = ({ getInstances, onRunQueries, onTest, queryRunner, queries }) => {
export const AlertingQueryPreview: FC<Props> = ({ getInstances, onRunQueries, onTest, queries, queryRunner }) => {
const [activeTab, setActiveTab] = useState<string>(Tabs.Query);
const styles = useStyles(getStyles);
const styles = getStyles(config.theme);
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
const data = useObservable<PanelData>(observable);
const instances = getInstances();
@ -49,59 +50,66 @@ export const AlertingQueryPreview: FC<Props> = ({ getInstances, onRunQueries, on
})}
</TabsBar>
<TabContent className={styles.tabContent}>
{data && data.state === 'Error' ? (
<div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>There was an error :(</h4>
<div>{data.error?.data?.error}</div>
</div>
) : queries && queries.length > 0 ? (
<AutoSizer style={{ width: '100%', height: '100%' }}>
{({ width, height }) => {
switch (activeTab) {
case Tabs.Instances:
return (
<PreviewInstancesTab
isTested={instances.length > 0}
instances={instances}
styles={styles}
width={width}
height={height}
onTest={onTest}
/>
);
case Tabs.Query:
default:
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>
) : (
<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>
)}
{data &&
(data.state === 'Error' ? (
<EmptyState title="There was an error :(">
<div>{data.error?.data?.error}</div>
</EmptyState>
) : (
<QueriesAndInstances
instances={instances}
onTest={onTest}
data={data}
activeTab={activeTab}
onRunQueries={onRunQueries}
queries={queries}
/>
))}
</TabContent>
</div>
);
};
interface PreviewProps {
queries: DataQuery[];
instances: DataFrame[];
onTest: () => void;
data: PanelData;
activeTab: string;
onRunQueries: () => void;
}
const QueriesAndInstances: FC<PreviewProps> = ({ queries, instances, onTest, data, activeTab, onRunQueries }) => {
if (queries.length === 0) {
return (
<EmptyState title="No queries added.">
<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>
</EmptyState>
);
}
return (
<AutoSizer style={{ width: '100%', height: '100%' }}>
{({ width, height }) => {
switch (activeTab) {
case Tabs.Instances:
return <PreviewInstancesTab instances={instances} width={width} height={height} onTest={onTest} />;
case Tabs.Query:
default:
return <PreviewQueryTab data={data} width={width} height={height} onRunQueries={onRunQueries} />;
}
}}
</AutoSizer>
);
};
const getStyles = (theme: GrafanaTheme) => {
return {
wrapper: css`
label: alertDefinitionPreviewTabs;
display: flex;
flex-direction: column;
width: 100%;
@ -112,19 +120,5 @@ const getStyles = (theme: GrafanaTheme) => {
background: ${theme.colors.panelBg};
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,37 @@
import React, { FC, ReactNode } from 'react';
import { css } from 'emotion';
import { useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
interface Props {
title: string;
children: ReactNode | ReactNode[];
}
export const EmptyState: FC<Props> = ({ children, title }) => {
const styles = useStyles(getStyles);
return (
<div className={styles.emptyState}>
<h4 className={styles.emptyStateHeader}>{title}</h4>
{children}
</div>
);
};
const getStyles = (theme: GrafanaTheme) => {
return {
emptyState: css`
color: ${theme.colors.textSemiWeak};
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
`,
emptyStateHeader: css`
color: ${theme.colors.textSemiWeak};
`,
};
};

View File

@ -1,25 +1,22 @@
import React, { FC } from 'react';
import { DataFrame } from '@grafana/data';
import { Button, Table } from '@grafana/ui';
import { PreviewStyles } from './AlertingQueryPreview';
import { EmptyState } from './EmptyState';
interface Props {
instances: DataFrame[];
isTested: boolean;
styles: PreviewStyles;
width: number;
height: number;
onTest: () => void;
}
export const PreviewInstancesTab: FC<Props> = ({ instances, isTested, onTest, height, styles, width }) => {
if (!isTested) {
export const PreviewInstancesTab: FC<Props> = ({ instances, onTest, height, width }) => {
if (!instances.length) {
return (
<div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>You havent tested your alert yet.</h4>
<EmptyState title="You havent tested your alert yet.">
<div>In order to see your instances, you need to test your alert first.</div>
<Button onClick={onTest}>Test alert now</Button>
</div>
</EmptyState>
);
}
return <Table data={instances[0]} height={height} width={width} />;

View File

@ -1,15 +1,17 @@
import React, { FC, useMemo, useState } from 'react';
import { getFrameDisplayName, GrafanaTheme, PanelData, SelectableValue, toDataFrame } from '@grafana/data';
import { Select, stylesFactory, Table, useTheme } from '@grafana/ui';
import { css } from 'emotion';
import { getFrameDisplayName, GrafanaTheme, PanelData, SelectableValue } from '@grafana/data';
import { Button, Select, stylesFactory, Table, useTheme } from '@grafana/ui';
import { EmptyState } from './EmptyState';
interface Props {
data?: PanelData;
data: PanelData;
width: number;
height: number;
onRunQueries: () => void;
}
export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => {
export const PreviewQueryTab: FC<Props> = ({ data, height, onRunQueries, width }) => {
const [currentSeries, setSeries] = useState<number>(0);
const theme = useTheme();
const styles = getStyles(theme, height);
@ -24,8 +26,16 @@ export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => {
// Select padding
const padding = 16;
if (!data?.series?.length) {
return <Table data={toDataFrame([])} height={height} width={width} />;
if (!data) {
return (
<EmptyState title="Run queries to view data.">
<Button onClick={onRunQueries}>Run queries</Button>
</EmptyState>
);
}
if (!data.series) {
return null;
}
if (data.series.length > 1) {
@ -48,12 +58,14 @@ export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => {
</div>
);
}
return <Table data={data.series[0]} height={height} width={width} />;
};
const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => {
return {
wrapper: css`
label: preview-wrapper;
height: ${height}px;
`,
selectWrapper: css`