Commit 6118ab41 by Peter Holmberg Committed by GitHub

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>
parent 5c9728a1
...@@ -196,6 +196,16 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto ...@@ -196,6 +196,16 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
}) })
} }
if hs.Cfg.IsNgAlertEnabled() {
navTree = append(navTree, &dtos.NavLink{
Text: "NgAlerting",
Id: "ngalerting",
SubTitle: "Next generation alerting",
Icon: "bell",
Url: setting.AppSubUrl + "/ngalerting",
})
}
if c.IsSignedIn { if c.IsSignedIn {
navTree = append(navTree, getProfileNode(c)) navTree = append(navTree, getProfileNode(c))
} }
......
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};
`,
};
});
...@@ -91,7 +91,7 @@ export class EditNotificationChannelPage extends PureComponent<Props> { ...@@ -91,7 +91,7 @@ export class EditNotificationChannelPage extends PureComponent<Props> {
return ( return (
<NotificationChannelForm <NotificationChannelForm
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes)} selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes, true)}
selectedChannel={selectedChannel} selectedChannel={selectedChannel}
imageRendererAvailable={config.rendererAvailable} imageRendererAvailable={config.rendererAvailable}
onTestChannel={this.onTestChannel} onTestChannel={this.onTestChannel}
......
...@@ -58,7 +58,7 @@ class NewNotificationChannelPage extends PureComponent<Props> { ...@@ -58,7 +58,7 @@ class NewNotificationChannelPage extends PureComponent<Props> {
return ( return (
<NotificationChannelForm <NotificationChannelForm
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes)} selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes, true)}
selectedChannel={selectedChannel} selectedChannel={selectedChannel}
onTestChannel={this.onTestChannel} onTestChannel={this.onTestChannel}
register={register} register={register}
......
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};
`,
};
});
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};
`,
};
};
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};
`,
};
});
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);
`,
};
};
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { notificationChannelLoaded, loadAlertRules, loadedAlertRules, setNotificationChannels } from './reducers'; import store from 'app/core/store';
import {
notificationChannelLoaded,
loadAlertRules,
loadedAlertRules,
setNotificationChannels,
setUiState,
ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
updateAlertDefinition,
setQueryOptions,
} from './reducers';
import { AlertDefinition, AlertDefinitionUiState, AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
import { QueryGroupOptions } from '../../query/components/QueryGroupOptions';
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> { export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
return async dispatch => { return async dispatch => {
...@@ -74,3 +85,56 @@ export function loadNotificationChannel(id: number): ThunkResult<void> { ...@@ -74,3 +85,56 @@ export function loadNotificationChannel(id: number): ThunkResult<void> {
dispatch(notificationChannelLoaded(notificationChannel)); dispatch(notificationChannelLoaded(notificationChannel));
}; };
} }
export function createAlertDefinition(): ThunkResult<void> {
return async (dispatch, getStore) => {
const alertDefinition: AlertDefinition = {
...getStore().alertDefinition.alertDefinition,
condition: {
ref: 'A',
queriesAndExpressions: [
{
model: {
expression: '2 + 2 > 1',
type: 'math',
datasource: '__expr__',
},
relativeTimeRange: {
From: 500,
To: 0,
},
refId: 'A',
},
],
},
};
await getBackendSrv().post(`/api/alert-definitions`, alertDefinition);
appEvents.emit(AppEvents.alertSuccess, ['Alert definition created']);
dispatch(updateLocation({ path: 'alerting/list' }));
};
}
export function updateAlertDefinitionUiState(uiState: Partial<AlertDefinitionUiState>): ThunkResult<void> {
return (dispatch, getStore) => {
const nextState = { ...getStore().alertDefinition.uiState, ...uiState };
dispatch(setUiState(nextState));
try {
store.setObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, nextState);
} catch (error) {
console.error(error);
}
};
}
export function updateAlertDefinitionOption(alertDefinition: Partial<AlertDefinition>): ThunkResult<void> {
return dispatch => {
dispatch(updateAlertDefinition(alertDefinition));
};
}
export function queryOptionsChange(queryOptions: QueryGroupOptions): ThunkResult<void> {
return dispatch => {
dispatch(setQueryOptions(queryOptions));
};
}
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ApplyFieldOverrideOptions, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data';
import alertDef from './alertDef';
import { import {
AlertCondition,
AlertDefinition,
AlertDefinitionState,
AlertDefinitionUiState,
AlertRule, AlertRule,
AlertRuleDTO, AlertRuleDTO,
AlertRulesState, AlertRulesState,
...@@ -6,9 +13,13 @@ import { ...@@ -6,9 +13,13 @@ import {
NotificationChannelState, NotificationChannelState,
NotifierDTO, NotifierDTO,
} from 'app/types'; } from 'app/types';
import alertDef from './alertDef'; import store from 'app/core/store';
import { dateTime } from '@grafana/data'; import { config } from '@grafana/runtime';
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 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 };
export const initialState: AlertRulesState = { export const initialState: AlertRulesState = {
items: [], items: [],
...@@ -22,6 +33,37 @@ export const initialChannelState: NotificationChannelState = { ...@@ -22,6 +33,37 @@ export const initialChannelState: NotificationChannelState = {
notifiers: [], notifiers: [],
}; };
const options: ApplyFieldOverrideOptions = {
fieldConfig: {
defaults: {
color: {
mode: FieldColorModeId.PaletteClassic,
},
},
overrides: [],
},
replaceVariables: (v: string) => v,
theme: config.theme,
};
const dataConfig = {
getTransformations: () => [] as DataTransformerConfig[],
getFieldOverrideOptions: () => options,
};
export const initialAlertDefinitionState: AlertDefinitionState = {
alertDefinition: {
id: 0,
name: '',
description: '',
condition: {} as AlertCondition,
},
queryOptions: { maxDataPoints: 100, dataSource: { name: 'gdev-testdata' }, queries: [] },
queryRunner: new PanelQueryRunner(dataConfig),
uiState: { ...store.getObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, DEFAULT_ALERT_DEFINITION_UI_STATE) },
data: [],
};
function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule { function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
const stateModel = alertDef.getStateDisplayModel(state); const stateModel = alertDef.getStateDisplayModel(state);
...@@ -108,6 +150,28 @@ const notificationChannelSlice = createSlice({ ...@@ -108,6 +150,28 @@ const notificationChannelSlice = createSlice({
}, },
}); });
const alertDefinitionSlice = createSlice({
name: 'alertDefinition',
initialState: initialAlertDefinitionState,
reducers: {
setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction<any>) => {
return { ...state, alertDefinition: action.payload };
},
updateAlertDefinition: (state: AlertDefinitionState, action: PayloadAction<Partial<AlertDefinition>>) => {
return { ...state, alertDefinition: { ...state.alertDefinition, ...action.payload } };
},
setUiState: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionUiState>) => {
return { ...state, uiState: { ...state.uiState, ...action.payload } };
},
setQueryOptions: (state: AlertDefinitionState, action: PayloadAction<QueryGroupOptions>) => {
return {
...state,
queryOptions: action.payload,
};
},
},
});
export const { loadAlertRules, loadedAlertRules, setSearchQuery } = alertRulesSlice.actions; export const { loadAlertRules, loadedAlertRules, setSearchQuery } = alertRulesSlice.actions;
export const { export const {
...@@ -116,12 +180,16 @@ export const { ...@@ -116,12 +180,16 @@ export const {
resetSecureField, resetSecureField,
} = notificationChannelSlice.actions; } = notificationChannelSlice.actions;
export const { setUiState, updateAlertDefinition, setQueryOptions } = alertDefinitionSlice.actions;
export const alertRulesReducer = alertRulesSlice.reducer; export const alertRulesReducer = alertRulesSlice.reducer;
export const notificationChannelReducer = notificationChannelSlice.reducer; export const notificationChannelReducer = notificationChannelSlice.reducer;
export const alertDefinitionsReducer = alertDefinitionSlice.reducer;
export default { export default {
alertRules: alertRulesReducer, alertRules: alertRulesReducer,
notificationChannel: notificationChannelReducer, notificationChannel: notificationChannelReducer,
alertDefinition: alertDefinitionsReducer,
}; };
function migrateSecureFields( function migrateSecureFields(
......
...@@ -22,12 +22,20 @@ export const defaultValues: NotificationChannelDTO = { ...@@ -22,12 +22,20 @@ export const defaultValues: NotificationChannelDTO = {
}; };
export const mapChannelsToSelectableValue = memoizeOne( export const mapChannelsToSelectableValue = memoizeOne(
(notificationChannels: NotificationChannelType[]): Array<SelectableValue<string>> => { (notificationChannels: NotificationChannelType[], includeDescription: boolean): Array<SelectableValue<string>> => {
return notificationChannels.map(channel => ({ return notificationChannels.map(channel => {
if (includeDescription) {
return {
value: channel.value, value: channel.value,
label: channel.label, label: channel.label,
description: channel.description, description: channel.description,
})); };
}
return {
value: channel.value,
label: channel.label,
};
});
} }
); );
......
...@@ -18,6 +18,7 @@ import { OptionsPaneContent } from './OptionsPaneContent'; ...@@ -18,6 +18,7 @@ import { OptionsPaneContent } from './OptionsPaneContent';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton'; import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems'; import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
import { BackButton } from 'app/core/components/BackButton/BackButton'; import { BackButton } from 'app/core/components/BackButton/BackButton';
import { PageToolbar } from 'app/core/components/PageToolbar/PageToolbar';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy'; import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
import { DashboardPanel } from '../../dashgrid/DashboardPanel'; import { DashboardPanel } from '../../dashgrid/DashboardPanel';
...@@ -245,41 +246,25 @@ export class PanelEditorUnconnected extends PureComponent<Props> { ...@@ -245,41 +246,25 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
); );
} }
editorToolbar(styles: EditorStyles) { renderEditorActions() {
const { dashboard } = this.props; return [
return (
<div className={styles.editorToolbar}>
<HorizontalGroup justify="space-between" align="center">
<div className={styles.toolbarLeft}>
<HorizontalGroup spacing="none">
<BackButton onClick={this.onPanelExit} surface="panel" />
<span className={styles.editorTitle}>{dashboard.title} / Edit Panel</span>
</HorizontalGroup>
</div>
<HorizontalGroup>
<HorizontalGroup spacing="sm" align="center">
<Button <Button
icon="cog" icon="cog"
onClick={this.onOpenDashboardSettings} onClick={this.onOpenDashboardSettings}
variant="secondary" variant="secondary"
title="Open dashboard settings" title="Open dashboard settings"
/> key="settings"
<Button onClick={this.onDiscard} variant="secondary" title="Undo all changes"> />,
<Button onClick={this.onDiscard} variant="secondary" title="Undo all changes" key="discard">
Discard Discard
</Button> </Button>,
<Button onClick={this.onSaveDashboard} variant="secondary" title="Apply changes and save dashboard"> <Button onClick={this.onSaveDashboard} variant="secondary" title="Apply changes and save dashboard" key="save">
Save Save
</Button> </Button>,
<Button onClick={this.onPanelExit} title="Apply changes and go back to dashboard"> <Button onClick={this.onPanelExit} title="Apply changes and go back to dashboard" key="apply">
Apply Apply
</Button> </Button>,
</HorizontalGroup> ];
</HorizontalGroup>
</HorizontalGroup>
</div>
);
} }
renderOptionsPane() { renderOptionsPane() {
...@@ -309,7 +294,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> { ...@@ -309,7 +294,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
} }
render() { render() {
const { initDone, updatePanelEditorUIState, uiState } = this.props; const { dashboard, initDone, updatePanelEditorUIState, uiState } = this.props;
const styles = getStyles(config.theme, this.props); const styles = getStyles(config.theme, this.props);
if (!initDone) { if (!initDone) {
...@@ -318,7 +303,11 @@ export class PanelEditorUnconnected extends PureComponent<Props> { ...@@ -318,7 +303,11 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
return ( return (
<div className={styles.wrapper} aria-label={selectors.components.PanelEditor.General.content}> <div className={styles.wrapper} aria-label={selectors.components.PanelEditor.General.content}>
{this.editorToolbar(styles)} <PageToolbar
title={`${dashboard.title} / Edit Panel`}
titlePrefix={<BackButton onClick={this.onPanelExit} surface="panel" />}
actions={this.renderEditorActions()}
/>
<div className={styles.verticalSplitPanesWrapper}> <div className={styles.verticalSplitPanesWrapper}>
<SplitPaneWrapper <SplitPaneWrapper
leftPaneComponents={this.renderPanelAndEditor(styles)} leftPaneComponents={this.renderPanelAndEditor(styles)}
...@@ -413,13 +402,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => { ...@@ -413,13 +402,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
height: 100%; height: 100%;
width: 100%; width: 100%;
`, `,
editorToolbar: css`
display: flex;
padding: ${theme.spacing.sm};
background: ${theme.colors.panelBg};
justify-content: space-between;
border-bottom: 1px solid ${theme.colors.panelBorder};
`,
panelToolbar: css` panelToolbar: css`
display: flex; display: flex;
padding: ${paneSpacing} 0 ${paneSpacing} ${paneSpacing}; padding: ${paneSpacing} 0 ${paneSpacing} ${paneSpacing};
...@@ -434,10 +416,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => { ...@@ -434,10 +416,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
`, `,
editorTitle: css`
font-size: ${theme.typography.size.lg};
padding-left: ${theme.spacing.md};
`,
}; };
}); });
......
...@@ -28,7 +28,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati ...@@ -28,7 +28,7 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
.when('/', { .when('/', {
template: '<react-container />', template: '<react-container />',
//@ts-ignore //@ts-ignore
pageClass: 'page-dashboard', pageClass: 'page-explore',
routeInfo: DashboardRouteInfo.Home, routeInfo: DashboardRouteInfo.Home,
reloadOnSearch: false, reloadOnSearch: false,
resolve: { resolve: {
...@@ -556,6 +556,17 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati ...@@ -556,6 +556,17 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
), ),
}, },
}) })
.when('/ngalerting', {
template: '<react-container />',
resolve: {
component: () =>
SafeDynamicImport(
import(/* webpackChunkName: "NgAlertingPage"*/ 'app/features/alerting/NextGenAlertingPage')
),
},
//@ts-ignore
pageClass: 'page-alerting',
})
.otherwise({ .otherwise({
template: '<react-container />', template: '<react-container />',
resolve: { resolve: {
......
import { SelectableValue } from '@grafana/data'; import { PanelData, SelectableValue } from '@grafana/data';
import { PanelQueryRunner } from '../features/query/state/PanelQueryRunner';
import { QueryGroupOptions } from '../features/query/components/QueryGroupOptions';
export interface AlertRuleDTO { export interface AlertRuleDTO {
id: number; id: number;
...@@ -133,3 +135,28 @@ export interface AlertNotification { ...@@ -133,3 +135,28 @@ export interface AlertNotification {
id: number; id: number;
type: string; type: string;
} }
export interface AlertDefinitionState {
uiState: AlertDefinitionUiState;
alertDefinition: AlertDefinition;
queryOptions: QueryGroupOptions;
queryRunner: PanelQueryRunner;
data: PanelData[];
}
export interface AlertDefinition {
id: number;
name: string;
description: string;
condition: AlertCondition;
}
export interface AlertCondition {
ref: string;
queriesAndExpressions: any[];
}
export interface AlertDefinitionUiState {
rightPaneSize: number;
topPaneSize: number;
}
...@@ -3,7 +3,7 @@ import { PayloadAction } from '@reduxjs/toolkit'; ...@@ -3,7 +3,7 @@ import { PayloadAction } from '@reduxjs/toolkit';
import { NavIndex } from '@grafana/data'; import { NavIndex } from '@grafana/data';
import { LocationState } from './location'; import { LocationState } from './location';
import { AlertRulesState, NotificationChannelState } from './alerting'; import { AlertDefinitionState, AlertRulesState, NotificationChannelState } from './alerting';
import { TeamsState, TeamState } from './teams'; import { TeamsState, TeamState } from './teams';
import { FolderState } from './folders'; import { FolderState } from './folders';
import { DashboardState } from './dashboard'; import { DashboardState } from './dashboard';
...@@ -45,6 +45,7 @@ export interface StoreState { ...@@ -45,6 +45,7 @@ export interface StoreState {
templating: TemplatingState; templating: TemplatingState;
importDashboard: ImportDashboardState; importDashboard: ImportDashboardState;
notificationChannel: NotificationChannelState; notificationChannel: NotificationChannelState;
alertDefinition: AlertDefinitionState;
} }
/* /*
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
background: $page-bg; background: $page-bg;
} }
.page-alerting,
.page-explore, .page-explore,
.page-dashboard { .page-dashboard {
.main-view { .main-view {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment