feat(alerts): create notification rule overlay (#14520)

* feat(rules): render rule overlay

* feat(ui/rules): add name and schedule to new overlay

* style(alerts): style new rule overlay like veo

* feat(alerts): add rule conditions and period count components

* feat(alerts): introduce StatusLevels component

* feat(alerts): add status change and level dropdowns

* refactor(alerts): use context to pass dispatch

* feat(rules): change check status level

* wip: wip

* test(alters): add cypress tests for create notification rule

* test(e2e): more testing for NewRuleOverlay

* feat(alerts): delete notification status rule

* feat(alerts): add TagRule component

* feat(alerts): remove TagRule

* feat(alerts): add RuleEndpoint dropdown

* feat(alerts): add notification rule message

* wip: upgrade clockface to 0.23

* styles(alerts): notification status rule

* styles(alerts): make new rule overlay pretty

* style(alerts): add DashedButton component

* test(e2e): fix notificationRule tests

* chore: remove feature flag

* style(alerts): LevelsDropdown

* chore: fix dummy data

* chore: capitalize LevelType

* style(alerts): copy css into NewRuleOverlay styles

* chore(alerts): add suffix  to client side interfaces

* chore(alerts): compile time reducer error defaults

* chore: plural to singular enums

* chore: trying new util convention

* chore: add back feature flag

* refactor(alterts): move state manipulation into reducer

* chore: comment feature flag

* refactor: move state changes into reducer
pull/14592/head
Andrew Watkins 2019-08-07 11:08:10 -07:00 committed by GitHub
parent f71885b742
commit 1d256551f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1545 additions and 48 deletions

View File

@ -0,0 +1,70 @@
describe('NotificationRules', () => {
beforeEach(() => {
cy.flush()
cy.signin().then(({body}) => {
const {
org: {id},
} = body
cy.wrap(body.org).as('org')
cy.fixture('routes').then(({orgs, alerting}) => {
cy.visit(`${orgs}/${id}${alerting}`)
})
})
})
it('can create a notification rule', () => {
cy.getByTestID('alert-column--header create-rule').click()
cy.getByTestID('rule-name--input').type('my-new-rule')
// Rule schedule section
cy.getByTestID('rule-schedule-cron').click()
cy.getByTestID('rule-schedule-cron--input')
.type('2 0 * * *')
.should('have.value', '2 0 * * *')
cy.getByTestID('rule-schedule-every').click()
cy.getByTestID('rule-schedule-every--input')
.type('20m')
.should('have.value', '20m')
cy.getByTestID('rule-schedule-offset--input')
.type('1m')
.should('have.value', '1m')
// Editing a Status Rule
cy.getByTestID('status-rule').within(() => {
cy.getByTestID('status-change--dropdown')
.click()
.within(() => {
cy.getByTestID('status-change--dropdown-item equal').click()
cy.getByTestID('status-change--dropdown--button').within(() => {
cy.contains('equal')
})
})
cy.getByTestID('levels--dropdown previousLevel').should('not.exist')
cy.getByTestID('levels--dropdown currentLevel').should('exist')
cy.getByTestID('status-change--dropdown')
.click()
.within(() => {
cy.getByTestID('status-change--dropdown-item changes from').click()
cy.getByTestID('status-change--dropdown--button').within(() => {
cy.contains('changes from')
})
})
cy.getByTestID('levels--dropdown previousLevel').click()
cy.getByTestID('levels--dropdown-item INFO').click()
cy.getByTestID('levels--dropdown--button previousLevel').within(() => {
cy.contains('INFO')
})
cy.getByTestID('levels--dropdown currentLevel').click()
cy.getByTestID('levels--dropdown-item CRIT').click()
cy.getByTestID('levels--dropdown--button currentLevel').within(() => {
cy.contains('CRIT')
})
})
})
})

View File

@ -1,5 +1,6 @@
{
"orgs": "/orgs",
"dashboards": "/dashboards",
"explorer": "/data-explorer"
"explorer": "/data-explorer",
"alerting": "/alerting"
}

View File

@ -68,24 +68,25 @@ export const changeCurrentCheckType = (type: CheckType) => ({
})
export const getChecks = () => async (
dispatch: Dispatch<Action | NotificationAction>,
getState: GetState
dispatch: Dispatch<Action | NotificationAction>
// getState: GetState
) => {
try {
dispatch(setAllChecks(RemoteDataState.Loading))
const {
orgs: {
org: {id: orgID},
},
} = getState()
// TODO: use this when its actually implemented
// const {
// orgs: {
// org: {id: orgID},
// },
// } = getState()
const resp = await api.getChecks({query: {orgID}})
// const resp = await api.getChecks({query: {orgID}})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
// if (resp.status !== 200) {
// throw new Error(resp.data.message)
// }
dispatch(setAllChecks(RemoteDataState.Done, resp.data.checks))
dispatch(setAllChecks(RemoteDataState.Done, []))
} catch (e) {
console.error(e)
dispatch(setAllChecks(RemoteDataState.Error))

View File

@ -2,25 +2,36 @@
import React, {FC} from 'react'
// Components
import {Button, IconFont} from '@influxdata/clockface'
const style = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}
import {
Button,
IconFont,
ComponentSpacer,
JustifyContent,
AlignItems,
FlexDirection,
} from '@influxdata/clockface'
interface Props {
title: string
testID?: string
onCreate: () => void
}
const AlertsColumnHeader: FC<Props> = ({onCreate, title}) => {
const AlertsColumnHeader: FC<Props> = ({onCreate, title, testID = ''}) => {
return (
<div style={style}>
{title}
<Button text="Create" icon={IconFont.AddCell} onClick={onCreate} />
</div>
<ComponentSpacer
direction={FlexDirection.Row}
justifyContent={JustifyContent.SpaceBetween}
alignItems={AlignItems.Center}
>
<div>{title}</div>
<Button
text="Create"
icon={IconFont.AddCell}
onClick={onCreate}
testID={`alert-column--header ${testID}`}
/>
</ComponentSpacer>
)
}

View File

@ -0,0 +1,87 @@
// Libraries
import React, {FC} from 'react'
// Components
import {
Dropdown,
ComponentColor,
InfluxColors,
DropdownMenuTheme,
} from '@influxdata/clockface'
// Types
import {CheckStatusLevel} from 'src/types'
type Level = CheckStatusLevel
type LevelType = 'currentLevel' | 'previousLevel'
type ColorLevel = {hex: InfluxColors; display: string; value: Level}
const levels: ColorLevel[] = [
{display: 'CRIT', hex: InfluxColors.Fire, value: 'CRIT'},
{display: 'INFO', hex: InfluxColors.Ocean, value: 'INFO'},
{display: 'WARN', hex: InfluxColors.Thunder, value: 'WARN'},
{display: 'OK', hex: InfluxColors.Viridian, value: 'OK'},
{display: 'UNKNOWN', hex: InfluxColors.Sidewalk, value: 'UNKNOWN'},
]
interface Props {
selectedLevel: Level
type: LevelType
onClickLevel: (type: LevelType, level: Level) => void
}
const LevelsDropdown: FC<Props> = ({type, selectedLevel, onClickLevel}) => {
const selected = levels.find(l => l.value === selectedLevel)
if (!selected) {
throw new Error('Unknown level type provided to <LevelsDropdown/>')
}
const button = (active, onClick) => (
<Dropdown.Button
color={ComponentColor.Default}
active={active}
onClick={onClick}
testID={`levels--dropdown--button ${type}`}
>
<div className="color-dropdown--item">
<div
className="color-dropdown--swatch"
style={{backgroundColor: selected.hex}}
/>
<div className="color-dropdown--name">{selected.value}</div>
</div>
</Dropdown.Button>
)
const items = levels.map(({value, display, hex}) => (
<Dropdown.Item
key={value}
id={value}
value={value}
onClick={() => onClickLevel(type, value)}
testID={`levels--dropdown-item ${value}`}
>
<div className="color-dropdown--item">
<div
className="color-dropdown--swatch"
style={{backgroundColor: hex}}
/>
<div className="color-dropdown--name">{display}</div>
</div>
</Dropdown.Item>
))
const menu = onCollapse => (
<Dropdown.Menu theme={DropdownMenuTheme.Onyx} onCollapse={onCollapse}>
{items}
</Dropdown.Menu>
)
return (
<Dropdown button={button} menu={menu} testID={`levels--dropdown ${type}`} />
)
}
export default LevelsDropdown

View File

@ -0,0 +1,67 @@
// Libraries
import {omit} from 'lodash'
// Types
import {
StatusRuleDraft,
LevelRule,
SlackBase,
SMTPBase,
PagerDutyBase,
} from 'src/types'
type EndpointBase = SlackBase | SMTPBase | PagerDutyBase
export const getEndpointBase = (endpoints, id: string): EndpointBase => {
const endpoint = endpoints.find(e => e.id === id)
switch (endpoint.type) {
case 'slack': {
return {messageTemplate: '', channel: '', type: 'slack'}
}
case 'smtp': {
return {to: '', bodyTemplate: '', subjectTemplate: '', type: 'smtp'}
}
case 'pagerduty': {
return {messageTemplate: '', type: 'pagerduty'}
}
default: {
throw new Error('Unknown endpoint type in <RuleMessage />')
}
}
}
type Change = 'changes from' | 'equal'
export const CHANGES: Change[] = ['changes from', 'equal']
export const activeChange = (status: StatusRuleDraft) => {
const {currentLevel, previousLevel} = status.value
if (!!previousLevel) {
return 'changes from'
}
if (currentLevel.operation === 'equal') {
return 'equal'
}
throw new Error(
'Changed statusRule.currentLevel.operation to unknown operator'
)
}
export const previousLevel: LevelRule = {level: 'OK'}
export const changeStatusRule = (
status: StatusRuleDraft,
change: Change
): StatusRuleDraft => {
if (change === 'equal') {
return omit(status, 'value.previousLevel') as StatusRuleDraft
}
const {value} = status
const newValue = {...value, previousLevel}
return {...status, value: newValue}
}

View File

@ -0,0 +1,149 @@
// Libraries
import {v4} from 'uuid'
// Types
import {
NotificationRuleDraft,
StatusRuleDraft,
TagRuleDraft,
CheckStatusLevel,
} from 'src/types'
export type LevelType = 'currentLevel' | 'previousLevel'
export type RuleState = NotificationRuleDraft
export type Action =
| {type: 'UPDATE_RULE'; rule: NotificationRuleDraft}
| {
type: 'UPDATE_STATUS_LEVEL'
statusID: string
levelType: LevelType
level: CheckStatusLevel
}
| {type: 'SET_ACTIVE_SCHEDULE'; schedule: 'cron' | 'every'}
| {type: 'UPDATE_STATUS_RULES'; statusRule: StatusRuleDraft}
| {type: 'ADD_TAG_RULE'; tagRule: TagRuleDraft}
| {type: 'DELETE_STATUS_RULE'; statusRuleID: string}
| {type: 'UPDATE_TAG_RULES'; tagRule: TagRuleDraft}
| {type: 'DELETE_TAG_RULE'; tagRuleID: string}
| {
type: 'SET_TAG_RULE_OPERATOR'
tagRuleID: string
operator: TagRuleDraft['value']['operator']
}
export const reducer = (state: RuleState, action: Action) => {
switch (action.type) {
case 'UPDATE_RULE': {
const {rule} = action
return {...state, ...rule}
}
case 'SET_ACTIVE_SCHEDULE': {
const {schedule} = action
return {...state, schedule}
}
case 'UPDATE_STATUS_RULES': {
const {statusRule} = action
const statusRules = state.statusRules.map(s => {
if (s.id !== statusRule.id) {
return s
}
return statusRule
})
return {...state, statusRules}
}
case 'ADD_TAG_RULE': {
const {tagRule} = action
return {
...state,
tagRules: [...state.tagRules, {...tagRule, id: v4()}],
}
}
case 'UPDATE_TAG_RULES': {
const {tagRule} = action
const tagRules = state.tagRules.map(t => {
if (t.id !== tagRule.id) {
return t
}
return tagRule
})
return {...state, tagRules}
}
case 'DELETE_STATUS_RULE': {
const {statusRuleID} = action
const statusRules = state.statusRules.filter(s => {
return s.id !== statusRuleID
})
return {
...state,
statusRules,
}
}
case 'DELETE_TAG_RULE': {
const {tagRuleID} = action
const tagRules = state.tagRules.filter(tr => {
return tr.id !== tagRuleID
})
return {...state, tagRules}
}
case 'UPDATE_STATUS_LEVEL': {
const {levelType, level, statusID} = action
const statusRules = state.statusRules.map(status => {
if (status.id !== statusID) {
return status
}
const value = {
...status.value,
[levelType]: {
...status.value[levelType],
level,
},
}
return {...status, value}
})
return {...state, statusRules}
}
case 'SET_TAG_RULE_OPERATOR': {
const {tagRuleID, operator} = action
const tagRules = state.tagRules.map(tagRule => {
if (tagRule.id !== tagRuleID) {
return tagRule
}
return {
...tagRule,
value: {
...tagRule.value,
operator,
},
}
})
return {...state, tagRules}
}
default:
const neverAction: never = action
throw new Error(
`Unhandled action: "${neverAction}" in NewRuleOverlay.reducer.ts`
)
}
}

View File

@ -0,0 +1,69 @@
.rule-eo {
width: calc(100vw - #{$ix-marg-e});
height: calc(100vh - #{$ix-marg-d});
margin: 0 auto;
display: flex;
flex-direction: column;
@include gradient-v($g2-kevlar, $g0-obsidian);
border-radius: 0 0 $radius $radius;
.rule-eo-contents {
padding-left: $ix-marg-d;
padding-right: $ix-marg-d;
}
.condition-row {
display: flex;
flex-direction: column;
background-color: $g5-pepper;
border-radius: $radius;
margin-bottom: 4px;
padding: 10px;
.period-count--container {
display: flex;
flex-direction: row;
margin-bottom: 4px;
align-items: center;
.count-input,
.period-input {
flex: 100px;
}
}
.status-levels--container {
display: flex;
flex-direction: row;
align-items: center;
}
.tag-rule {
display: flex;
flex-direction: row;
}
}
.sentence-frag {
background-color: $g7-graphite;
border-radius: $radius;
font-size: 12px;
padding: 0 9px;
height: 30px;
line-height: 26px;
text-align: center;
font-weight: bold;
}
.color-dropdown--item {
display: flex;
align-items: center;
justify-content: flex-start;
}
.color-dropdown--swatch {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 5px;
}
}

View File

@ -1,27 +1,95 @@
// Libraries
import React, {FC} from 'react'
import React, {FC, useReducer, Dispatch} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
// Components
import {Overlay} from '@influxdata/clockface'
import RuleSchedule from 'src/alerting/components/notifications/RuleSchedule'
import RuleConditions from 'src/alerting/components/notifications/RuleConditions'
import RuleMessage from 'src/alerting/components/notifications/RuleMessage'
import {
Panel,
ComponentSize,
Overlay,
Form,
Input,
Grid,
Columns,
} from '@influxdata/clockface'
// Reducers
import {reducer, RuleState, Action} from './NewRuleOverlay.reducer'
// Constants
import {newRule, endpoints} from 'src/alerting/constants'
// Types
import {NotificationRuleDraft} from 'src/types'
type Props = WithRouterProps
export const newRuleState: RuleState = {
...newRule,
schedule: 'every',
}
export const NewRuleDispatch = React.createContext<Dispatch<Action>>(null)
const NewRuleOverlay: FC<Props> = ({params, router}) => {
const handleDismiss = () => {
router.push(`/orgs/${params.orgID}/alerting`)
}
const [rule, dispatch] = useReducer(reducer, newRuleState)
const handleChange = e => {
const {name, value} = e.target
dispatch({
type: 'UPDATE_RULE',
rule: {...rule, [name]: value} as NotificationRuleDraft,
})
}
return (
<Overlay visible={true}>
<Overlay.Container>
<Overlay.Header
title="Create a Notification Rule"
onDismiss={handleDismiss}
/>
<Overlay.Body>hai</Overlay.Body>
</Overlay.Container>
</Overlay>
<NewRuleDispatch.Provider value={dispatch}>
<Overlay visible={true}>
<Overlay.Container maxWidth={800}>
<Overlay.Header
title="Create a Notification Rule"
onDismiss={handleDismiss}
/>
<Overlay.Body>
<Grid>
<Form>
<Grid.Row>
<Grid.Column widthSM={Columns.Two}>About</Grid.Column>
<Grid.Column widthSM={Columns.Ten}>
<Panel size={ComponentSize.ExtraSmall}>
<Panel.Body>
<Form.Element label="Name">
<Input
testID="rule-name--input"
placeholder="Name this new rule"
value={rule.name}
name="name"
onChange={handleChange}
/>
</Form.Element>
<RuleSchedule rule={rule} onChange={handleChange} />
</Panel.Body>
</Panel>
</Grid.Column>
<Grid.Column>
<hr />
</Grid.Column>
</Grid.Row>
<RuleConditions rule={rule} />
<RuleMessage rule={rule} endpoints={endpoints} />
</Form>
</Grid>
</Overlay.Body>
</Overlay.Container>
</Overlay>
</NewRuleDispatch.Provider>
)
}

View File

@ -0,0 +1,24 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {Form, TextArea} from '@influxdata/clockface'
interface Props {
messageTemplate: string
onChange: (e: ChangeEvent) => void
}
const PagerDutyMessage: FC<Props> = ({messageTemplate, onChange}) => {
return (
<Form.Element label="Message">
<TextArea
name="messageTemplate"
onChange={onChange}
value={messageTemplate}
/>
</Form.Element>
)
}
export default PagerDutyMessage

View File

@ -0,0 +1,38 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {Input, InputType} from '@influxdata/clockface'
interface Props {
period: string
count: number
onChange: (e: ChangeEvent<HTMLInputElement>) => void
}
const PeriodCount: FC<Props> = ({period, count, onChange}) => {
return (
<div className="period-count--container">
<Input
className="count-input"
type={InputType.Number}
name="count"
value={count}
placeholder="1"
onChange={onChange}
testID="count--input"
/>
<div className="sentence-frag">instances in the last</div>
<Input
className="period-input"
name="period"
value={period}
placeholder="1h"
onChange={onChange}
testID="period--input"
/>
</div>
)
}
export default PeriodCount

View File

@ -10,7 +10,7 @@ import NotificationRuleCardContext from 'src/alerting/components/notifications/R
// Constants
import {DEFAULT_NOTIFICATION_RULE_NAME} from 'src/alerting/constants'
// Actions
// Action
import {updateRule, deleteRule} from 'src/alerting/actions/notifications/rules'
// Types

View File

@ -0,0 +1,68 @@
// Libraries
import React, {FC, useContext} from 'react'
// Components
import {
Grid,
Columns,
ComponentSpacer,
FlexDirection,
ComponentSize,
AlignItems,
} from '@influxdata/clockface'
import StatusRuleComponent from 'src/alerting/components/notifications/StatusRule'
import TagRuleComponent from 'src/alerting/components/notifications/TagRule'
import {NewRuleDispatch} from 'src/alerting/components/notifications/NewRuleOverlay'
import DashedButton from 'src/shared/components/dashed_button/DashedButton'
// Constants
import {newTagRule} from 'src/alerting/constants'
// Types
import {RuleState} from './NewRuleOverlay.reducer'
interface Props {
rule: RuleState
}
const RuleConditions: FC<Props> = ({rule}) => {
const dispatch = useContext(NewRuleDispatch)
const {statusRules, tagRules} = rule
const addTagRule = () => {
dispatch({
type: 'ADD_TAG_RULE',
tagRule: newTagRule,
})
}
const statuses = statusRules.map(status => (
<StatusRuleComponent key={status.id} status={status} />
))
const tags = tagRules.map(tagRule => (
<TagRuleComponent key={tagRule.id} tagRule={tagRule} />
))
return (
<Grid.Row>
<Grid.Column widthSM={Columns.Two}>Conditions</Grid.Column>
<Grid.Column widthSM={Columns.Ten}>
<ComponentSpacer
direction={FlexDirection.Column}
margin={ComponentSize.Small}
alignItems={AlignItems.Stretch}
>
{statuses}
{tags}
<DashedButton text="+ Tag Rule" onClick={addTagRule} />
</ComponentSpacer>
</Grid.Column>
<Grid.Column>
<hr />
</Grid.Column>
</Grid.Row>
)
}
export default RuleConditions

View File

@ -0,0 +1,59 @@
// Libraries
import React, {FC} from 'react'
// Components
import {Dropdown} from '@influxdata/clockface'
// Types
import {NotificationEndpoint} from 'src/types'
interface Props {
selectedEndpointID: string
endpoints: NotificationEndpoint[]
onSelectEndpoint: (endpointID: string) => void
}
const RuleEndpointDropdown: FC<Props> = ({
endpoints,
selectedEndpointID,
onSelectEndpoint,
}) => {
const items = endpoints.map(({id, type, name}) => (
<Dropdown.Item
key={id}
id={id}
value={id}
testID={`endpoint--dropdown-item ${type}`}
onClick={() => onSelectEndpoint(id)}
>
{name}
</Dropdown.Item>
))
const selectedEndpoint = endpoints.find(e => e.id === selectedEndpointID)
const button = (active, onClick) => (
<Dropdown.Button
testID="endpoint--dropdown--button"
active={active}
onClick={onClick}
>
{selectedEndpoint.name}
</Dropdown.Button>
)
const menu = onCollapse => (
<Dropdown.Menu onCollapse={onCollapse}>{items}</Dropdown.Menu>
)
return (
<Dropdown
button={button}
menu={menu}
widthPixels={160}
testID="status-change--dropdown"
/>
)
}
export default RuleEndpointDropdown

View File

@ -0,0 +1,57 @@
// Libraries
import React, {FC, useContext} from 'react'
// Components
import {Form, Panel, Grid, Columns} from '@influxdata/clockface'
import {NewRuleDispatch} from 'src/alerting/components/notifications/NewRuleOverlay'
import RuleEndpointDropdown from 'src/alerting/components/notifications/RuleEndpointDropdown'
import RuleMessageContents from 'src/alerting/components/notifications/RuleMessageContents'
// Types
import {NotificationEndpoint, NotificationRuleDraft} from 'src/types'
// Utils
import {getEndpointBase} from './NewRule.utils'
interface Props {
endpoints: NotificationEndpoint[]
rule: NotificationRuleDraft
}
const RuleMessage: FC<Props> = ({endpoints, rule}) => {
const dispatch = useContext(NewRuleDispatch)
const onSelectEndpoint = notifyEndpointID => {
const endpoint = getEndpointBase(endpoints, notifyEndpointID)
dispatch({
type: 'UPDATE_RULE',
rule: {
...rule,
...endpoint,
notifyEndpointID,
},
})
}
return (
<Grid.Row>
<Grid.Column widthSM={Columns.Two}>Message</Grid.Column>
<Grid.Column widthSM={Columns.Ten}>
<Panel>
<Panel.Body>
<Form.Element label="Endpoint">
<RuleEndpointDropdown
endpoints={endpoints}
onSelectEndpoint={onSelectEndpoint}
selectedEndpointID={rule.notifyEndpointID}
/>
</Form.Element>
<RuleMessageContents rule={rule} />
</Panel.Body>
</Panel>
</Grid.Column>
</Grid.Row>
)
}
export default RuleMessage

View File

@ -0,0 +1,68 @@
// Libraries
import React, {FC, useContext} from 'react'
// Components
import SlackMessage from './SlackMessage'
import SMTPMessage from './SMTPMessage'
import PagerDutyMessage from './PagerDutyMessage'
import {NewRuleDispatch} from './NewRuleOverlay'
// Types
import {NotificationRuleDraft} from 'src/types'
interface Props {
rule: NotificationRuleDraft
}
const RuleMessageContents: FC<Props> = ({rule}) => {
const dispatch = useContext(NewRuleDispatch)
const onChange = ({target}) => {
const {name, value} = target
dispatch({
type: 'UPDATE_RULE',
rule: {
...rule,
[name]: value,
},
})
}
switch (rule.type) {
case 'slack': {
const {messageTemplate, channel} = rule
return (
<SlackMessage
messageTemplate={messageTemplate}
channel={channel}
onChange={onChange}
/>
)
}
case 'smtp': {
const {to, subjectTemplate, bodyTemplate} = rule
return (
<SMTPMessage
to={to}
onChange={onChange}
bodyTemplate={bodyTemplate}
subjectTemplate={subjectTemplate}
/>
)
}
case 'pagerduty': {
const {messageTemplate} = rule
return (
<PagerDutyMessage
messageTemplate={messageTemplate}
onChange={onChange}
/>
)
}
default:
throw new Error('Unexpected endpoint type in <RuleMessageContents/>.')
}
}
export default RuleMessageContents

View File

@ -0,0 +1,98 @@
// Libraries
import React, {FC, ChangeEvent, useContext} from 'react'
// Components
import {
Radio,
Form,
Input,
InputType,
Grid,
Columns,
ButtonShape,
} from '@influxdata/clockface'
import {NewRuleDispatch} from './NewRuleOverlay'
// Types
import {RuleState} from './NewRuleOverlay.reducer'
interface Props {
rule: RuleState
onChange: (e: ChangeEvent) => void
}
const RuleSchedule: FC<Props> = ({rule, onChange}) => {
const {schedule, cron, every, offset} = rule
const label = schedule === 'every' ? 'Every' : 'Cron'
const placeholder = schedule === 'every' ? '1d3h30s' : '0 2 * * *'
const value = schedule === 'every' ? every : cron
const dispatch = useContext(NewRuleDispatch)
return (
<Grid.Row>
<Grid.Column widthXS={Columns.Four}>
<Form.Element label="Schedule">
<Radio shape={ButtonShape.StretchToFit}>
<Radio.Button
id="every"
testID="rule-schedule-every"
active={schedule === 'every'}
value="every"
titleText="Run task at regular intervals"
onClick={() =>
dispatch({
type: 'SET_ACTIVE_SCHEDULE',
schedule: 'every',
})
}
>
Every
</Radio.Button>
<Radio.Button
id="cron"
testID="rule-schedule-cron"
active={schedule === 'cron'}
value="cron"
titleText="Use cron syntax for more control over scheduling"
onClick={() =>
dispatch({
type: 'SET_ACTIVE_SCHEDULE',
schedule: 'cron',
})
}
>
Cron
</Radio.Button>
</Radio>
</Form.Element>
</Grid.Column>
<Grid.Column widthXS={Columns.Four}>
<Form.Element label={label}>
<Input
value={value}
name={schedule}
type={InputType.Text}
placeholder={placeholder}
onChange={onChange}
testID={`rule-schedule-${schedule}--input`}
/>
</Form.Element>
</Grid.Column>
<Grid.Column widthXS={Columns.Four}>
<Form.Element label="Offset">
<Input
name="offset"
type={InputType.Text}
value={offset}
placeholder="20m"
onChange={onChange}
testID="rule-schedule-offset--input"
/>
</Form.Element>
</Grid.Column>
</Grid.Row>
)
}
export default RuleSchedule

View File

@ -1,6 +1,7 @@
// Libraries
import React, {FunctionComponent} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
// Types
import {NotificationRule, AppState} from 'src/types'
@ -11,14 +12,25 @@ interface StateProps {
rules: NotificationRule[]
}
type Props = StateProps
type Props = StateProps & WithRouterProps
const NotificationRulesColumn: FunctionComponent<Props> = ({rules}) => {
const handleClick = () => {}
const NotificationRulesColumn: FunctionComponent<Props> = ({
rules,
router,
params,
}) => {
const handleOpenOverlay = () => {
const newRuleRoute = `/orgs/${params.orgID}/alerting/rules/new`
router.push(newRuleRoute)
}
return (
<>
<AlertsColumnHeader title="Notification Rules" onCreate={handleClick} />
<AlertsColumnHeader
title="Notification Rules"
testID="create-rule"
onCreate={handleOpenOverlay}
/>
<NotificationRuleCards rules={rules} />
</>
)
@ -35,4 +47,4 @@ const mstp = (state: AppState) => {
export default connect<StateProps, {}, {}>(
mstp,
null
)(NotificationRulesColumn)
)(withRouter(NotificationRulesColumn))

View File

@ -0,0 +1,43 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {Form, Input, TextArea} from '@influxdata/clockface'
interface Props {
to: string
subjectTemplate
bodyTemplate: string
onChange: (e: ChangeEvent) => void
}
const SMTPMessage: FC<Props> = ({
to,
subjectTemplate,
bodyTemplate,
onChange,
}) => {
return (
<>
<Form.Element label="To">
<Input value={to} name="to" onChange={onChange} />
</Form.Element>
<Form.Element label="Subject">
<Input
value={subjectTemplate}
name="subjectTemplate"
onChange={onChange}
/>
</Form.Element>
<Form.Element label="Body">
<TextArea
name="bodyTemplate"
value={bodyTemplate}
onChange={onChange}
/>
</Form.Element>
</>
)
}
export default SMTPMessage

View File

@ -0,0 +1,30 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {Form, Input, TextArea} from '@influxdata/clockface'
interface Props {
channel: string
messageTemplate: string
onChange: (e: ChangeEvent) => void
}
const SlackMessage: FC<Props> = ({channel, messageTemplate, onChange}) => {
return (
<>
<Form.Element label="Channel">
<Input value={channel} name="channel" onChange={onChange} />
</Form.Element>
<Form.Element label="Message">
<TextArea
name="messageTemplate"
value={messageTemplate}
onChange={onChange}
/>
</Form.Element>
</>
)
}
export default SlackMessage

View File

@ -0,0 +1,60 @@
// Libraries
import React, {FC, useContext} from 'react'
// Types
import {StatusRuleDraft} from 'src/types'
// Components
import {Dropdown} from '@influxdata/clockface'
import {NewRuleDispatch} from 'src/alerting/components/notifications/NewRuleOverlay'
// Utils
import {CHANGES, changeStatusRule, activeChange} from './NewRule.utils'
interface Props {
status: StatusRuleDraft
}
const StatusChangeDropdown: FC<Props> = ({status}) => {
const dispatch = useContext(NewRuleDispatch)
const statusChange = (s, c) =>
dispatch({
type: 'UPDATE_STATUS_RULES',
statusRule: changeStatusRule(s, c),
})
const items = CHANGES.map(change => (
<Dropdown.Item
key={change}
id={change}
value={change}
testID={`status-change--dropdown-item ${change}`}
onClick={() => statusChange(status, change)}
>
{change}
</Dropdown.Item>
))
const buttonText = activeChange(status)
const button = (active, onClick) => (
<Dropdown.Button
testID="status-change--dropdown--button"
active={active}
onClick={onClick}
>
{buttonText}
</Dropdown.Button>
)
const menu = onCollapse => (
<Dropdown.Menu onCollapse={onCollapse}>{items}</Dropdown.Menu>
)
return (
<Dropdown button={button} menu={menu} testID="status-change--dropdown" />
)
}
export default StatusChangeDropdown

View File

@ -0,0 +1,63 @@
// Libraries
import React, {FC, useContext} from 'react'
// Components
import {
ComponentSpacer,
TextBlock,
FlexDirection,
ComponentSize,
} from '@influxdata/clockface'
import {NewRuleDispatch} from './NewRuleOverlay'
import LevelsDropdown from 'src/alerting/components/notifications/LevelsDropdown'
import StatusChangeDropdown from 'src/alerting/components/notifications/StatusChangeDropdown'
import {LevelType} from 'src/alerting/components/notifications/NewRuleOverlay.reducer'
// Types
import {StatusRuleDraft, CheckStatusLevel} from 'src/types'
interface Props {
status: StatusRuleDraft
}
const StatusLevels: FC<Props> = ({status}) => {
const {currentLevel, previousLevel} = status.value
const dispatch = useContext(NewRuleDispatch)
const onClickLevel = (levelType: LevelType, level: CheckStatusLevel) => {
dispatch({
type: 'UPDATE_STATUS_LEVEL',
statusID: status.id,
levelType,
level,
})
}
return (
<ComponentSpacer direction={FlexDirection.Row} margin={ComponentSize.Small}>
<TextBlock text="When status" />
<ComponentSpacer.FlexChild grow={0} basis={140}>
<StatusChangeDropdown status={status} />
</ComponentSpacer.FlexChild>
{!!previousLevel && (
<ComponentSpacer.FlexChild grow={0} basis={140}>
<LevelsDropdown
type="previousLevel"
selectedLevel={previousLevel.level}
onClickLevel={onClickLevel}
/>
</ComponentSpacer.FlexChild>
)}
{!!previousLevel && <TextBlock text="to" />}
<ComponentSpacer.FlexChild grow={0} basis={140}>
<LevelsDropdown
type="currentLevel"
selectedLevel={currentLevel.level}
onClickLevel={onClickLevel}
/>
</ComponentSpacer.FlexChild>
</ComponentSpacer>
)
}
export default StatusLevels

View File

@ -0,0 +1,25 @@
// Libraries
import React, {FC} from 'react'
// Components
import {Panel, ComponentSize} from '@influxdata/clockface'
import StatusLevels from 'src/alerting/components/notifications/StatusLevels'
// Types
import {StatusRuleDraft} from 'src/types'
interface Props {
status: StatusRuleDraft
}
const StatusRuleComponent: FC<Props> = ({status}) => {
return (
<Panel size={ComponentSize.ExtraSmall} testID="status-rule">
<Panel.Body>
<StatusLevels status={status} />
</Panel.Body>
</Panel>
)
}
export default StatusRuleComponent

View File

@ -0,0 +1,102 @@
// Libraries
import React, {FC, useContext} from 'react'
// Components
import {
Input,
Panel,
DismissButton,
TextBlock,
ComponentSpacer,
ComponentSize,
FlexDirection,
ComponentColor,
} from '@influxdata/clockface'
import {NewRuleDispatch} from 'src/alerting/components/notifications/NewRuleOverlay'
import TagRuleOperatorDropdown, {
Operator,
} from 'src/alerting/components/notifications/TagRuleOperatorDropdown'
// Types
import {TagRuleDraft} from 'src/types'
interface Props {
tagRule: TagRuleDraft
}
const TagRule: FC<Props> = ({tagRule}) => {
const {key, value, operator} = tagRule.value
const dispatch = useContext(NewRuleDispatch)
const onChange = ({target}) => {
const {name, value} = target
const newValue = {
...tagRule.value,
[name]: value,
}
dispatch({
type: 'UPDATE_TAG_RULES',
tagRule: {
...tagRule,
value: newValue,
},
})
}
const onSelectOperator = (operator: Operator) => {
dispatch({
type: 'SET_TAG_RULE_OPERATOR',
tagRuleID: tagRule.id,
operator,
})
}
const onDelete = () => {
dispatch({
type: 'DELETE_TAG_RULE',
tagRuleID: tagRule.id,
})
}
return (
<Panel testID="tag-rule" size={ComponentSize.ExtraSmall}>
<DismissButton onClick={onDelete} color={ComponentColor.Default} />
<Panel.Body>
<ComponentSpacer
direction={FlexDirection.Row}
margin={ComponentSize.Small}
>
<TextBlock text="When tag" />
<ComponentSpacer.FlexChild grow={1}>
<Input
testID="tag-rule-key--input"
placeholder="Key"
value={key}
name="key"
onChange={onChange}
/>
</ComponentSpacer.FlexChild>
<ComponentSpacer.FlexChild grow={0} basis={60}>
<TagRuleOperatorDropdown
selectedOperator={operator}
onSelect={onSelectOperator}
/>
</ComponentSpacer.FlexChild>
<ComponentSpacer.FlexChild grow={1}>
<Input
testID="tag-rule-key--input"
placeholder="Value"
value={value}
name="value"
onChange={onChange}
/>
</ComponentSpacer.FlexChild>
</ComponentSpacer>
</Panel.Body>
</Panel>
)
}
export default TagRule

View File

@ -0,0 +1,56 @@
// Libraries
import React, {FC} from 'react'
// Components
import {Dropdown} from '@influxdata/clockface'
// Types
import {TagRuleDraft} from 'src/types'
interface Props {
selectedOperator: Operator
onSelect: (operator: Operator) => void
}
export type Operator = TagRuleDraft['value']['operator']
const operators: {operator: Operator; display: string}[] = [
{operator: 'equal', display: '=='},
{operator: 'notequal', display: '!='},
{operator: 'equalregex', display: '=~'},
{operator: 'notequalregex', display: '!~'},
]
const TagRuleOperatorDropdown: FC<Props> = ({selectedOperator, onSelect}) => {
const items = operators.map(({operator, display}) => (
<Dropdown.Item
key={operator}
id={operator}
value={display}
testID={`tag-rule--dropdown-item ${operator}`}
onClick={() => onSelect(operator)}
>
{display}
</Dropdown.Item>
))
const buttonText = operators.find(o => o.operator === selectedOperator)
const button = (active, onClick) => (
<Dropdown.Button
testID="tag-rule--dropdown--button"
active={active}
onClick={onClick}
>
{buttonText.display}
</Dropdown.Button>
)
const menu = onCollapse => (
<Dropdown.Menu onCollapse={onCollapse}>{items}</Dropdown.Menu>
)
return <Dropdown menu={menu} button={button} testID="tag-rule--dropdown" />
}
export default TagRuleOperatorDropdown

View File

@ -3,8 +3,12 @@ import {
DashboardQuery,
NotificationRule,
ThresholdCheck,
StatusRuleDraft,
TagRuleDraft,
NotificationRuleDraft,
DeadmanCheck,
} from 'src/types'
import {NotificationEndpoint} from 'src/client'
export const DEFAULT_CHECK_NAME = 'Name this check'
export const DEFAULT_NOTIFICATION_RULE_NAME = 'Name this notification rule'
@ -86,6 +90,76 @@ export const check2: Check = {
export const checks: Array<Check> = [check1, check2]
export const newStatusRule: StatusRuleDraft = {
id: '',
value: {
currentLevel: {
operation: 'equal',
level: 'WARN',
},
previousLevel: {
operation: 'equal',
level: 'OK',
},
period: '1h',
count: 1,
},
}
export const newTagRule: TagRuleDraft = {
id: '',
value: {
key: '',
value: '',
operator: 'equal',
},
}
export const newRule: NotificationRuleDraft = {
id: '',
notifyEndpointID: '1',
type: 'slack',
every: '',
orgID: '',
name: '',
schedule: 'every',
status: 'active',
messageTemplate: '',
tagRules: [newTagRule],
statusRules: [newStatusRule],
description: '',
}
export const endpoints: NotificationEndpoint[] = [
{
id: '1',
orgID: '1',
userID: '1',
description: 'interrupt everyone at work',
name: 'slack endpoint',
status: 'active',
type: 'slack',
},
{
id: '2',
orgID: '1',
userID: '1',
description: 'interrupt someone by email',
name: 'smtp endpoint',
status: 'active',
type: 'smtp',
},
{
id: '3',
orgID: '1',
userID: '1',
description: 'interrupt someone by all means known to man',
name: 'pagerditty endpoint',
status: 'active',
type: 'pagerduty',
},
]
export const rule: NotificationRule = {
id: '3',
notifyEndpointID: '2',

View File

@ -29,6 +29,7 @@ describe('rulesReducer', () => {
expect(actual).toEqual(expected)
})
})
describe('setRule', () => {
it('adds rule to list if it is new', () => {
const initialState = defaultNotificationRulesState
@ -42,6 +43,7 @@ describe('rulesReducer', () => {
expect(actual).toEqual(expected)
})
it('updates rule in list if it exists', () => {
let initialState = defaultNotificationRulesState
initialState.list = [rule]
@ -62,6 +64,7 @@ describe('rulesReducer', () => {
expect(actual).toEqual(expected)
})
})
describe('removeRule', () => {
it('removes rule from list', () => {
const initialState = defaultNotificationRulesState
@ -76,6 +79,7 @@ describe('rulesReducer', () => {
expect(actual).toEqual(expected)
})
})
describe('setCurrentRule', () => {
it('sets current rule and status.', () => {
const initialState = defaultNotificationRulesState

View File

@ -1,6 +1,6 @@
// Libraries
import React, {SFC} from 'react'
import _ from 'lodash'
import React, {FC} from 'react'
import {capitalize} from 'lodash'
// Components
import {
@ -26,7 +26,7 @@ interface DefaultProps {
type Props = PassedProps & DefaultProps
const ColorDropdown: SFC<Props> = props => {
const ColorDropdown: FC<Props> = props => {
const {
selected,
colors,
@ -50,7 +50,7 @@ const ColorDropdown: SFC<Props> = props => {
style={{backgroundColor: selected.hex}}
/>
<div className="color-dropdown--name">
{_.capitalize(selected.name)}
{capitalize(selected.name)}
</div>
</div>
</Dropdown.Button>
@ -71,7 +71,7 @@ const ColorDropdown: SFC<Props> = props => {
style={{backgroundColor: color.hex}}
/>
<div className="color-dropdown--name">
{_.capitalize(color.name)}
{capitalize(color.name)}
</div>
</div>
</Dropdown.Item>

View File

@ -0,0 +1,25 @@
.dashed-button {
width: 100%;
border: $ix-border dashed $g4-onyx;
background-color: rgba($g5-pepper, 0);
color: $g9-mountain;
height: 54px;
transition: background-color 0.25s ease, color 0.25s ease,
border-color 0.25s ease;
font-size: 13px;
font-weight: 700;
outline: none;
border-radius: $ix-radius;
&:hover {
cursor: pointer;
background-color: rgba($g5-pepper, 0.25);
border-color: $g6-smoke;
color: $g13-mist;
}
&:active {
border-color: $c-pool;
color: $c-pool;
}
}

View File

@ -0,0 +1,17 @@
// Libraries
import React, {FC, MouseEvent} from 'react'
interface Props {
text: string
onClick: (e: MouseEvent) => void
}
const DashedButton: FC<Props> = ({text, onClick}) => {
return (
<button className="dashed-button" onClick={onClick}>
{text}
</button>
)
}
export default DashedButton

View File

@ -28,6 +28,7 @@ export const isFlagEnabled = (flagName: string) => {
)
}
// type influx.toggleFeature('myFlag') to disable / enable any feature flag
export const FeatureFlag: FunctionComponent<{name: string}> = ({
name,
children,

View File

@ -112,7 +112,8 @@
@import 'src/members/components/AddMembersForm.scss';
@import 'src/shared/components/DeleteDataForm/DeleteDataForm.scss';
@import 'src/shared/components/cloud/CloudOnly.scss';
@import 'src/alerting/components/notifications/NewRuleOverlay.scss';
@import 'src/shared/components/dashed_button/DashedButton.scss';
// External
@import '../../node_modules/@influxdata/react-custom-scrollbars/dist/styles.css';

View File

@ -1,9 +1,58 @@
import {StatusRule, NotificationRuleBase, TagRule} from 'src/client'
export interface AddID<T> {
id: string
value: T
}
export type StatusRuleDraft = AddID<StatusRule>
export type TagRuleDraft = AddID<TagRule>
type ExcludeKeys<T> = Pick<T, Exclude<keyof T, 'statusRules' | 'tagRules'>>
export interface NotificationRuleBaseBox
extends ExcludeKeys<NotificationRuleBase> {
schedule: 'cron' | 'every'
statusRules: StatusRuleDraft[]
tagRules: TagRuleDraft[]
}
export type NotificationRuleDraft = SlackRule | SMTPRule | PagerDutyRule
export type SlackBase = {
type: 'slack'
channel?: string
messageTemplate: string
}
type SlackRule = NotificationRuleBaseBox & SlackBase
export type SMTPBase = {
type: 'smtp'
to: string
bodyTemplate?: string
subjectTemplate: string
}
type SMTPRule = NotificationRuleBaseBox & SMTPBase
export type PagerDutyBase = {
type: 'pagerduty'
messageTemplate: string
}
type PagerDutyRule = NotificationRuleBaseBox & PagerDutyBase
export {
Check,
CheckBase,
StatusRule,
LevelRule,
TagRule,
CheckStatusLevel,
ThresholdCheck,
DeadmanCheck,
NotificationEndpoint,
NotificationRuleBase,
NotificationRule,
SMTPNotificationRule,