mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
@@ -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 {
|
||||
navTree = append(navTree, getProfileNode(c))
|
||||
}
|
||||
|
51
public/app/core/components/PageToolbar/PageToolbar.tsx
Normal file
51
public/app/core/components/PageToolbar/PageToolbar.tsx
Normal file
@@ -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};
|
||||
`,
|
||||
};
|
||||
});
|
@@ -91,7 +91,7 @@ export class EditNotificationChannelPage extends PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<NotificationChannelForm
|
||||
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes)}
|
||||
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes, true)}
|
||||
selectedChannel={selectedChannel}
|
||||
imageRendererAvailable={config.rendererAvailable}
|
||||
onTestChannel={this.onTestChannel}
|
||||
|
@@ -58,7 +58,7 @@ class NewNotificationChannelPage extends PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<NotificationChannelForm
|
||||
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes)}
|
||||
selectableChannels={mapChannelsToSelectableValue(notificationChannelTypes, true)}
|
||||
selectedChannel={selectedChannel}
|
||||
onTestChannel={this.onTestChannel}
|
||||
register={register}
|
||||
|
140
public/app/features/alerting/NextGenAlertingPage.tsx
Normal file
140
public/app/features/alerting/NextGenAlertingPage.tsx
Normal file
@@ -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);
|
||||
`,
|
||||
};
|
||||
};
|
@@ -1,9 +1,20 @@
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
|
||||
import { appEvents } from 'app/core/core';
|
||||
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> {
|
||||
return async dispatch => {
|
||||
@@ -74,3 +85,56 @@ export function loadNotificationChannel(id: number): ThunkResult<void> {
|
||||
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));
|
||||
};
|
||||
}
|
||||
|
@@ -1,4 +1,11 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { ApplyFieldOverrideOptions, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data';
|
||||
import alertDef from './alertDef';
|
||||
import {
|
||||
AlertCondition,
|
||||
AlertDefinition,
|
||||
AlertDefinitionState,
|
||||
AlertDefinitionUiState,
|
||||
AlertRule,
|
||||
AlertRuleDTO,
|
||||
AlertRulesState,
|
||||
@@ -6,9 +13,13 @@ import {
|
||||
NotificationChannelState,
|
||||
NotifierDTO,
|
||||
} from 'app/types';
|
||||
import alertDef from './alertDef';
|
||||
import { dateTime } from '@grafana/data';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import store from 'app/core/store';
|
||||
import { config } from '@grafana/runtime';
|
||||
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 = {
|
||||
items: [],
|
||||
@@ -22,6 +33,37 @@ export const initialChannelState: NotificationChannelState = {
|
||||
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 {
|
||||
const stateModel = alertDef.getStateDisplayModel(state);
|
||||
|
||||
@@ -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 {
|
||||
@@ -116,12 +180,16 @@ export const {
|
||||
resetSecureField,
|
||||
} = notificationChannelSlice.actions;
|
||||
|
||||
export const { setUiState, updateAlertDefinition, setQueryOptions } = alertDefinitionSlice.actions;
|
||||
|
||||
export const alertRulesReducer = alertRulesSlice.reducer;
|
||||
export const notificationChannelReducer = notificationChannelSlice.reducer;
|
||||
export const alertDefinitionsReducer = alertDefinitionSlice.reducer;
|
||||
|
||||
export default {
|
||||
alertRules: alertRulesReducer,
|
||||
notificationChannel: notificationChannelReducer,
|
||||
alertDefinition: alertDefinitionsReducer,
|
||||
};
|
||||
|
||||
function migrateSecureFields(
|
||||
|
@@ -22,12 +22,20 @@ export const defaultValues: NotificationChannelDTO = {
|
||||
};
|
||||
|
||||
export const mapChannelsToSelectableValue = memoizeOne(
|
||||
(notificationChannels: NotificationChannelType[]): Array<SelectableValue<string>> => {
|
||||
return notificationChannels.map(channel => ({
|
||||
value: channel.value,
|
||||
label: channel.label,
|
||||
description: channel.description,
|
||||
}));
|
||||
(notificationChannels: NotificationChannelType[], includeDescription: boolean): Array<SelectableValue<string>> => {
|
||||
return notificationChannels.map(channel => {
|
||||
if (includeDescription) {
|
||||
return {
|
||||
value: channel.value,
|
||||
label: channel.label,
|
||||
description: channel.description,
|
||||
};
|
||||
}
|
||||
return {
|
||||
value: channel.value,
|
||||
label: channel.label,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import { OptionsPaneContent } from './OptionsPaneContent';
|
||||
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
|
||||
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
|
||||
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 { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
|
||||
import { DashboardPanel } from '../../dashgrid/DashboardPanel';
|
||||
@@ -245,41 +246,25 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
editorToolbar(styles: EditorStyles) {
|
||||
const { dashboard } = this.props;
|
||||
|
||||
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
|
||||
icon="cog"
|
||||
onClick={this.onOpenDashboardSettings}
|
||||
variant="secondary"
|
||||
title="Open dashboard settings"
|
||||
/>
|
||||
<Button onClick={this.onDiscard} variant="secondary" title="Undo all changes">
|
||||
Discard
|
||||
</Button>
|
||||
<Button onClick={this.onSaveDashboard} variant="secondary" title="Apply changes and save dashboard">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={this.onPanelExit} title="Apply changes and go back to dashboard">
|
||||
Apply
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
renderEditorActions() {
|
||||
return [
|
||||
<Button
|
||||
icon="cog"
|
||||
onClick={this.onOpenDashboardSettings}
|
||||
variant="secondary"
|
||||
title="Open dashboard settings"
|
||||
key="settings"
|
||||
/>,
|
||||
<Button onClick={this.onDiscard} variant="secondary" title="Undo all changes" key="discard">
|
||||
Discard
|
||||
</Button>,
|
||||
<Button onClick={this.onSaveDashboard} variant="secondary" title="Apply changes and save dashboard" key="save">
|
||||
Save
|
||||
</Button>,
|
||||
<Button onClick={this.onPanelExit} title="Apply changes and go back to dashboard" key="apply">
|
||||
Apply
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
|
||||
renderOptionsPane() {
|
||||
@@ -309,7 +294,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { initDone, updatePanelEditorUIState, uiState } = this.props;
|
||||
const { dashboard, initDone, updatePanelEditorUIState, uiState } = this.props;
|
||||
const styles = getStyles(config.theme, this.props);
|
||||
|
||||
if (!initDone) {
|
||||
@@ -318,7 +303,11 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<SplitPaneWrapper
|
||||
leftPaneComponents={this.renderPanelAndEditor(styles)}
|
||||
@@ -413,13 +402,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
|
||||
height: 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`
|
||||
display: flex;
|
||||
padding: ${paneSpacing} 0 ${paneSpacing} ${paneSpacing};
|
||||
@@ -434,10 +416,6 @@ export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
|
||||
justify-content: 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
|
||||
.when('/', {
|
||||
template: '<react-container />',
|
||||
//@ts-ignore
|
||||
pageClass: 'page-dashboard',
|
||||
pageClass: 'page-explore',
|
||||
routeInfo: DashboardRouteInfo.Home,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
@@ -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({
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
|
@@ -1,4 +1,6 @@
|
||||
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 {
|
||||
id: number;
|
||||
@@ -133,3 +135,28 @@ export interface AlertNotification {
|
||||
id: number;
|
||||
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';
|
||||
import { NavIndex } from '@grafana/data';
|
||||
|
||||
import { LocationState } from './location';
|
||||
import { AlertRulesState, NotificationChannelState } from './alerting';
|
||||
import { AlertDefinitionState, AlertRulesState, NotificationChannelState } from './alerting';
|
||||
import { TeamsState, TeamState } from './teams';
|
||||
import { FolderState } from './folders';
|
||||
import { DashboardState } from './dashboard';
|
||||
@@ -45,6 +45,7 @@ export interface StoreState {
|
||||
templating: TemplatingState;
|
||||
importDashboard: ImportDashboardState;
|
||||
notificationChannel: NotificationChannelState;
|
||||
alertDefinition: AlertDefinitionState;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -14,6 +14,7 @@
|
||||
background: $page-bg;
|
||||
}
|
||||
|
||||
.page-alerting,
|
||||
.page-explore,
|
||||
.page-dashboard {
|
||||
.main-view {
|
||||
|
Reference in New Issue
Block a user