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
5 changed files with 127 additions and 85 deletions
@@ -3,8 +3,8 @@ import { hot } from 'react-hot-loader';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { css } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui'; import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { AlertingQueryEditor } from './components/AlertingQueryEditor'; import { AlertingQueryEditor } from './components/AlertingQueryEditor';
@@ -97,7 +97,9 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
} }
}; };
onDiscard = () => {}; onDiscard = () => {
locationService.replace(`${config.appSubUrl}/alerting/list`);
};
onTest = () => { onTest = () => {
const { alertDefinition, evaluateAlertDefinition, evaluateNotSavedAlertDefinition } = this.props; const { alertDefinition, evaluateAlertDefinition, evaluateNotSavedAlertDefinition } = this.props;
@@ -125,12 +127,12 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
render() { render() {
const { const {
alertDefinition, alertDefinition,
getInstances,
uiState, uiState,
updateAlertDefinitionUiState, updateAlertDefinitionUiState,
queryRunner,
getQueryOptions, getQueryOptions,
getInstances,
onRunQueries, onRunQueries,
queryRunner,
} = this.props; } = this.props;
const styles = getStyles(config.theme); const styles = getStyles(config.theme);
@@ -146,10 +148,10 @@ class NextGenAlertingPageUnconnected extends PureComponent<Props> {
leftPaneComponents={[ leftPaneComponents={[
<AlertingQueryPreview <AlertingQueryPreview
key="queryPreview" 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} onTest={this.onTest}
queries={queryOptions.queries}
getInstances={getInstances}
queryRunner={queryRunner!}
onRunQueries={onRunQueries} onRunQueries={onRunQueries}
/>, />,
<AlertingQueryEditor key="queryEditor" />, <AlertingQueryEditor key="queryEditor" />,
@@ -3,10 +3,12 @@ import { useObservable } from 'react-use';
import { css } from 'emotion'; import { css } from 'emotion';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { DataFrame, DataQuery, GrafanaTheme, PanelData } from '@grafana/data'; import { DataFrame, DataQuery, GrafanaTheme, PanelData } from '@grafana/data';
import { Button, Icon, Tab, TabContent, TabsBar, useStyles } from '@grafana/ui'; import { config } from '@grafana/runtime';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { Icon, Tab, TabContent, TabsBar } from '@grafana/ui';
import { PreviewQueryTab } from './PreviewQueryTab'; import { PreviewQueryTab } from './PreviewQueryTab';
import { PreviewInstancesTab } from './PreviewInstancesTab'; import { PreviewInstancesTab } from './PreviewInstancesTab';
import { EmptyState } from './EmptyState';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
enum Tabs { enum Tabs {
Query = 'query', Query = 'query',
@@ -26,10 +28,9 @@ interface Props {
onRunQueries: () => void; 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 [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 observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
const data = useObservable<PanelData>(observable); const data = useObservable<PanelData>(observable);
const instances = getInstances(); const instances = getInstances();
@@ -49,59 +50,66 @@ export const AlertingQueryPreview: FC<Props> = ({ getInstances, onRunQueries, on
})} })}
</TabsBar> </TabsBar>
<TabContent className={styles.tabContent}> <TabContent className={styles.tabContent}>
{data && data.state === 'Error' ? ( {data &&
<div className={styles.noQueries}> (data.state === 'Error' ? (
<h4 className={styles.noQueriesHeader}>There was an error :(</h4> <EmptyState title="There was an error :(">
<div>{data.error?.data?.error}</div> <div>{data.error?.data?.error}</div>
</div> </EmptyState>
) : queries && queries.length > 0 ? ( ) : (
<AutoSizer style={{ width: '100%', height: '100%' }}> <QueriesAndInstances
{({ width, height }) => { instances={instances}
switch (activeTab) { onTest={onTest}
case Tabs.Instances: data={data}
return ( activeTab={activeTab}
<PreviewInstancesTab onRunQueries={onRunQueries}
isTested={instances.length > 0} queries={queries}
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>
)}
</TabContent> </TabContent>
</div> </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) => { const getStyles = (theme: GrafanaTheme) => {
return { return {
wrapper: css` wrapper: css`
label: alertDefinitionPreviewTabs;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@@ -112,19 +120,5 @@ const getStyles = (theme: GrafanaTheme) => {
background: ${theme.colors.panelBg}; background: ${theme.colors.panelBg};
height: 100%; 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>;
@@ -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};
`,
};
};
@@ -1,25 +1,22 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { DataFrame } from '@grafana/data'; import { DataFrame } from '@grafana/data';
import { Button, Table } from '@grafana/ui'; import { Button, Table } from '@grafana/ui';
import { PreviewStyles } from './AlertingQueryPreview'; import { EmptyState } from './EmptyState';
interface Props { interface Props {
instances: DataFrame[]; instances: DataFrame[];
isTested: boolean;
styles: PreviewStyles;
width: number; width: number;
height: number; height: number;
onTest: () => void; onTest: () => void;
} }
export const PreviewInstancesTab: FC<Props> = ({ instances, isTested, onTest, height, styles, width }) => { export const PreviewInstancesTab: FC<Props> = ({ instances, onTest, height, width }) => {
if (!isTested) { if (!instances.length) {
return ( return (
<div className={styles.noQueries}> <EmptyState title="You haven’t tested your alert yet.">
<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> <div>In order to see your instances, you need to test your alert first.</div>
<Button onClick={onTest}>Test alert now</Button> <Button onClick={onTest}>Test alert now</Button>
</div> </EmptyState>
); );
} }
return <Table data={instances[0]} height={height} width={width} />; return <Table data={instances[0]} height={height} width={width} />;
@@ -1,15 +1,17 @@
import React, { FC, useMemo, useState } from 'react'; 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 { 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 { interface Props {
data?: PanelData; data: PanelData;
width: number; width: number;
height: 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 [currentSeries, setSeries] = useState<number>(0);
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme, height); const styles = getStyles(theme, height);
@@ -24,8 +26,16 @@ export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => {
// Select padding // Select padding
const padding = 16; const padding = 16;
if (!data?.series?.length) { if (!data) {
return <Table data={toDataFrame([])} height={height} width={width} />; 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) { if (data.series.length > 1) {
@@ -48,12 +58,14 @@ export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => {
</div> </div>
); );
} }
return <Table data={data.series[0]} height={height} width={width} />; return <Table data={data.series[0]} height={height} width={width} />;
}; };
const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => {
return { return {
wrapper: css` wrapper: css`
label: preview-wrapper;
height: ${height}px; height: ${height}px;
`, `,
selectWrapper: css` selectWrapper: css`