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 reducerpull/14592/head
parent
f71885b742
commit
1d256551f6
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"orgs": "/orgs",
|
||||
"dashboards": "/dashboards",
|
||||
"explorer": "/data-explorer"
|
||||
"explorer": "/data-explorer",
|
||||
"alerting": "/alerting"
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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}
|
||||
}
|
|
@ -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`
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue