mirror of https://github.com/grafana/grafana
Alerting: Next gen Alerting page (#28397)
* create page and sidebar entry * add components for query editor and definition * split pane things * add reducer and action * implement split pane and update ui actions * making things pretty * Unify toolbar * minor tweak to title prefix and some padding * can create definitions * fix default state * add notificaion channel * add wrappers to get correct spacing between panes * include or exclude description * implement query editor * start on query result component * update from master * some cleanup and remove expressions touch ups Co-authored-by: Torkel Ödegaard <torkel@grafana.com>pull/29777/head
parent
5c9728a1c2
commit
6118ab415d
@ -0,0 +1,51 @@ |
|||||||
|
import React, { FC, ReactNode } from 'react'; |
||||||
|
import { css } from 'emotion'; |
||||||
|
import { GrafanaTheme } from '@grafana/data'; |
||||||
|
import { HorizontalGroup, stylesFactory, useTheme } from '@grafana/ui'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
title: string; |
||||||
|
titlePrefix?: ReactNode; |
||||||
|
actions: ReactNode[]; |
||||||
|
titlePadding?: 'sm' | 'lg'; |
||||||
|
} |
||||||
|
|
||||||
|
export const PageToolbar: FC<Props> = ({ actions, title, titlePrefix, titlePadding = 'lg' }) => { |
||||||
|
const styles = getStyles(useTheme(), titlePadding); |
||||||
|
return ( |
||||||
|
<div className={styles.toolbarWrapper}> |
||||||
|
<HorizontalGroup justify="space-between" align="center"> |
||||||
|
<div className={styles.toolbarLeft}> |
||||||
|
<HorizontalGroup spacing="none"> |
||||||
|
{titlePrefix} |
||||||
|
<span className={styles.toolbarTitle}>{title}</span> |
||||||
|
</HorizontalGroup> |
||||||
|
</div> |
||||||
|
<HorizontalGroup spacing="sm" align="center"> |
||||||
|
{actions} |
||||||
|
</HorizontalGroup> |
||||||
|
</HorizontalGroup> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme, padding: string) => { |
||||||
|
const titlePadding = padding === 'sm' ? theme.spacing.sm : theme.spacing.md; |
||||||
|
|
||||||
|
return { |
||||||
|
toolbarWrapper: css` |
||||||
|
display: flex; |
||||||
|
padding: ${theme.spacing.sm}; |
||||||
|
background: ${theme.colors.panelBg}; |
||||||
|
justify-content: space-between; |
||||||
|
border-bottom: 1px solid ${theme.colors.panelBorder}; |
||||||
|
`,
|
||||||
|
toolbarLeft: css` |
||||||
|
padding-left: ${theme.spacing.sm}; |
||||||
|
`,
|
||||||
|
toolbarTitle: css` |
||||||
|
font-size: ${theme.typography.size.lg}; |
||||||
|
padding-left: ${titlePadding}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}); |
@ -0,0 +1,140 @@ |
|||||||
|
import React, { FormEvent, PureComponent } from 'react'; |
||||||
|
import { hot } from 'react-hot-loader'; |
||||||
|
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; |
||||||
|
import { css } from 'emotion'; |
||||||
|
import { GrafanaTheme } from '@grafana/data'; |
||||||
|
import { Button, Icon, stylesFactory } from '@grafana/ui'; |
||||||
|
import { PageToolbar } from 'app/core/components/PageToolbar/PageToolbar'; |
||||||
|
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; |
||||||
|
import AlertingQueryEditor from './components/AlertingQueryEditor'; |
||||||
|
import { AlertDefinitionOptions } from './components/AlertDefinitionOptions'; |
||||||
|
import { AlertingQueryPreview } from './components/AlertingQueryPreview'; |
||||||
|
import { |
||||||
|
updateAlertDefinitionOption, |
||||||
|
createAlertDefinition, |
||||||
|
updateAlertDefinitionUiState, |
||||||
|
loadNotificationTypes, |
||||||
|
} from './state/actions'; |
||||||
|
import { AlertDefinition, AlertDefinitionUiState, NotificationChannelType, StoreState } from '../../types'; |
||||||
|
|
||||||
|
import { config } from 'app/core/config'; |
||||||
|
import { PanelQueryRunner } from '../query/state/PanelQueryRunner'; |
||||||
|
|
||||||
|
interface OwnProps {} |
||||||
|
|
||||||
|
interface ConnectedProps { |
||||||
|
alertDefinition: AlertDefinition; |
||||||
|
uiState: AlertDefinitionUiState; |
||||||
|
notificationChannelTypes: NotificationChannelType[]; |
||||||
|
queryRunner: PanelQueryRunner; |
||||||
|
} |
||||||
|
|
||||||
|
interface DispatchProps { |
||||||
|
createAlertDefinition: typeof createAlertDefinition; |
||||||
|
updateAlertDefinitionUiState: typeof updateAlertDefinitionUiState; |
||||||
|
updateAlertDefinitionOption: typeof updateAlertDefinitionOption; |
||||||
|
loadNotificationTypes: typeof loadNotificationTypes; |
||||||
|
} |
||||||
|
|
||||||
|
interface State {} |
||||||
|
|
||||||
|
type Props = OwnProps & ConnectedProps & DispatchProps; |
||||||
|
|
||||||
|
class NextGenAlertingPage extends PureComponent<Props, State> { |
||||||
|
state = { dataSources: [] }; |
||||||
|
|
||||||
|
componentDidMount() { |
||||||
|
this.props.loadNotificationTypes(); |
||||||
|
} |
||||||
|
|
||||||
|
onChangeAlertOption = (event: FormEvent<HTMLFormElement>) => { |
||||||
|
this.props.updateAlertDefinitionOption({ [event.currentTarget.name]: event.currentTarget.value }); |
||||||
|
}; |
||||||
|
|
||||||
|
onSaveAlert = () => { |
||||||
|
const { createAlertDefinition } = this.props; |
||||||
|
|
||||||
|
createAlertDefinition(); |
||||||
|
}; |
||||||
|
|
||||||
|
onDiscard = () => {}; |
||||||
|
|
||||||
|
onTest = () => {}; |
||||||
|
|
||||||
|
renderToolbarActions() { |
||||||
|
return [ |
||||||
|
<Button variant="destructive" key="discard" onClick={this.onDiscard}> |
||||||
|
Discard |
||||||
|
</Button>, |
||||||
|
<Button variant="primary" key="save" onClick={this.onSaveAlert}> |
||||||
|
Save |
||||||
|
</Button>, |
||||||
|
<Button variant="secondary" key="test" onClick={this.onTest}> |
||||||
|
Test |
||||||
|
</Button>, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
const { |
||||||
|
alertDefinition, |
||||||
|
notificationChannelTypes, |
||||||
|
uiState, |
||||||
|
updateAlertDefinitionUiState, |
||||||
|
queryRunner, |
||||||
|
} = this.props; |
||||||
|
const styles = getStyles(config.theme); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.wrapper}> |
||||||
|
<PageToolbar |
||||||
|
title="Alert editor" |
||||||
|
titlePrefix={<Icon name="bell" size="lg" />} |
||||||
|
actions={this.renderToolbarActions()} |
||||||
|
titlePadding="sm" |
||||||
|
/> |
||||||
|
<SplitPaneWrapper |
||||||
|
leftPaneComponents={[ |
||||||
|
<AlertingQueryPreview key="queryPreview" queryRunner={queryRunner} />, |
||||||
|
<AlertingQueryEditor key="queryEditor" />, |
||||||
|
]} |
||||||
|
uiState={uiState} |
||||||
|
updateUiState={updateAlertDefinitionUiState} |
||||||
|
rightPaneComponents={ |
||||||
|
<AlertDefinitionOptions |
||||||
|
alertDefinition={alertDefinition} |
||||||
|
onChange={this.onChangeAlertOption} |
||||||
|
notificationChannelTypes={notificationChannelTypes} |
||||||
|
/> |
||||||
|
} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = state => { |
||||||
|
return { |
||||||
|
uiState: state.alertDefinition.uiState, |
||||||
|
alertDefinition: state.alertDefinition.alertDefinition, |
||||||
|
notificationChannelTypes: state.notificationChannel.notificationChannelTypes, |
||||||
|
queryRunner: state.alertDefinition.queryRunner, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { |
||||||
|
createAlertDefinition, |
||||||
|
updateAlertDefinitionUiState, |
||||||
|
updateAlertDefinitionOption, |
||||||
|
loadNotificationTypes, |
||||||
|
}; |
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(NextGenAlertingPage)); |
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => { |
||||||
|
return { |
||||||
|
wrapper: css` |
||||||
|
background-color: ${theme.colors.dashboardBg}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}); |
@ -0,0 +1,55 @@ |
|||||||
|
import React, { FC, FormEvent } from 'react'; |
||||||
|
import { css } from 'emotion'; |
||||||
|
import { GrafanaTheme } from '@grafana/data'; |
||||||
|
import { Field, Input, Select, TextArea, useStyles } from '@grafana/ui'; |
||||||
|
import { AlertDefinition, NotificationChannelType } from 'app/types'; |
||||||
|
import { mapChannelsToSelectableValue } from '../utils/notificationChannels'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
alertDefinition: AlertDefinition; |
||||||
|
notificationChannelTypes: NotificationChannelType[]; |
||||||
|
onChange: (event: FormEvent) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const AlertDefinitionOptions: FC<Props> = ({ alertDefinition, notificationChannelTypes, onChange }) => { |
||||||
|
const styles = useStyles(getStyles); |
||||||
|
|
||||||
|
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} /> |
||||||
|
</Field> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme) => { |
||||||
|
return { |
||||||
|
wrapper: css` |
||||||
|
padding-top: ${theme.spacing.md}; |
||||||
|
`,
|
||||||
|
container: css` |
||||||
|
padding: ${theme.spacing.md}; |
||||||
|
background-color: ${theme.colors.panelBg}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,91 @@ |
|||||||
|
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 { 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'; |
||||||
|
|
||||||
|
interface OwnProps {} |
||||||
|
|
||||||
|
interface ConnectedProps { |
||||||
|
queryOptions: QueryGroupOptions; |
||||||
|
queryRunner: PanelQueryRunner; |
||||||
|
} |
||||||
|
interface DispatchProps { |
||||||
|
queryOptionsChange: typeof queryOptionsChange; |
||||||
|
} |
||||||
|
|
||||||
|
type Props = ConnectedProps & DispatchProps & OwnProps; |
||||||
|
|
||||||
|
export class AlertingQueryEditor extends PureComponent<Props> { |
||||||
|
onQueryOptionsChange = (queryOptions: QueryGroupOptions) => { |
||||||
|
this.props.queryOptionsChange(queryOptions); |
||||||
|
}; |
||||||
|
|
||||||
|
onRunQueries = () => { |
||||||
|
const { queryRunner, queryOptions } = this.props; |
||||||
|
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!, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
render() { |
||||||
|
const { queryOptions, queryRunner } = this.props; |
||||||
|
const styles = getStyles(config.theme); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.wrapper}> |
||||||
|
<div className={styles.container}> |
||||||
|
<h4>Queries</h4> |
||||||
|
<QueryGroup |
||||||
|
queryRunner={queryRunner} |
||||||
|
options={queryOptions} |
||||||
|
onRunQueries={this.onRunQueries} |
||||||
|
onOptionsChange={this.onQueryOptionsChange} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = state => { |
||||||
|
return { |
||||||
|
queryOptions: state.alertDefinition.queryOptions, |
||||||
|
queryRunner: state.alertDefinition.queryRunner, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { |
||||||
|
queryOptionsChange, |
||||||
|
}; |
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AlertingQueryEditor); |
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => { |
||||||
|
return { |
||||||
|
wrapper: css` |
||||||
|
padding-left: ${theme.spacing.md}; |
||||||
|
`,
|
||||||
|
container: css` |
||||||
|
padding: ${theme.spacing.md}; |
||||||
|
background-color: ${theme.colors.panelBg}; |
||||||
|
`,
|
||||||
|
editorWrapper: css` |
||||||
|
border: 1px solid ${theme.colors.panelBorder}; |
||||||
|
border-radius: ${theme.border.radius.md}; |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}); |
@ -0,0 +1,69 @@ |
|||||||
|
import React, { FC, useMemo, useState } from 'react'; |
||||||
|
import { useObservable } from 'react-use'; |
||||||
|
import { css } from 'emotion'; |
||||||
|
import { GrafanaTheme } from '@grafana/data'; |
||||||
|
import { TabsBar, TabContent, Tab, useStyles, Table } from '@grafana/ui'; |
||||||
|
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; |
||||||
|
|
||||||
|
enum Tabs { |
||||||
|
Query = 'query', |
||||||
|
Instance = 'instance', |
||||||
|
} |
||||||
|
|
||||||
|
const tabs = [ |
||||||
|
{ id: Tabs.Query, text: 'Query', active: true }, |
||||||
|
{ id: Tabs.Instance, text: 'Alerting instance', active: false }, |
||||||
|
]; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
queryRunner: PanelQueryRunner; |
||||||
|
} |
||||||
|
|
||||||
|
export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => { |
||||||
|
const [activeTab, setActiveTab] = useState<string>('query'); |
||||||
|
const styles = useStyles(getStyles); |
||||||
|
|
||||||
|
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []); |
||||||
|
const data = useObservable(observable); |
||||||
|
return ( |
||||||
|
<div className={styles.wrapper}> |
||||||
|
<TabsBar> |
||||||
|
{tabs.map((tab, index) => { |
||||||
|
return ( |
||||||
|
<Tab |
||||||
|
key={`${tab.id}-${index}`} |
||||||
|
label={tab.text} |
||||||
|
onChangeTab={() => setActiveTab(tab.id)} |
||||||
|
active={activeTab === tab.id} |
||||||
|
/> |
||||||
|
); |
||||||
|
})} |
||||||
|
</TabsBar> |
||||||
|
<TabContent className={styles.tabContent}> |
||||||
|
{activeTab === Tabs.Query && data && ( |
||||||
|
<div> |
||||||
|
<Table data={data.series[0]} width={1200} height={300} /> |
||||||
|
</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; |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
padding: ${theme.spacing.md} 0 0 ${theme.spacing.md}; |
||||||
|
`,
|
||||||
|
tabContent: css` |
||||||
|
background: ${theme.colors.panelBg}; |
||||||
|
height: calc(100% - ${tabBarHeight}px); |
||||||
|
`,
|
||||||
|
}; |
||||||
|
}; |
Loading…
Reference in new issue