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