Add alerting cards (#14429)

* bump to client 0.5.3

* export Query type from client and create alerting mock

* Bump client to 0.5.5

* Merge all status enums to one type

* Fix threshold visualization types

* Add extra underscore to notification rules

* Use check view type from client

* Add description field to checks, notificationrules and endpoints

* Add init check cards

* Add check card actions

* Correct component name

* Add check card context and toggle

* Add id to notification rule base

* Add notification rule cards

* add checks reducer tests

* user immer for checks reducer

* add tests and immer to notification rule reducer
pull/14433/head
Deniz Kusefoglu 2019-07-24 00:34:42 -07:00 committed by GitHub
parent da4615ea17
commit 613aef698f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1014 additions and 229 deletions

View File

@ -6497,12 +6497,7 @@ components:
description: An optional description of the task.
type: string
status:
description: The current status of the task. When updated to 'inactive', cancels all queued jobs of this task.
default: active
type: string
enum:
- active
- inactive
$ref: "#/components/schemas/TaskStatusType"
labels:
$ref: "#/components/schemas/Labels"
authorizationID:
@ -6557,6 +6552,9 @@ components:
labels:
$ref: "#/components/schemas/Link"
required: [id, name, orgID, flux]
TaskStatusType:
type: string
enum: [active, inactive]
User:
properties:
id:
@ -8684,10 +8682,7 @@ components:
query:
$ref: "#/components/schemas/DashboardQuery"
status:
description: The status of the check task.
default: active
type: string
enum: ["active", "inactive"]
$ref: "#/components/schemas/TaskStatusType"
every:
description: Check repetition interval
type: string
@ -8707,6 +8702,9 @@ components:
type: string
value:
type: string
description:
description: An optional description of the check
type: string
statusMessageTemplate:
description: template that is used to generate and write a status message
type: string
@ -8812,6 +8810,9 @@ components:
NotificationRuleBase:
type: object
properties:
id:
readOnly: true
type: string
notifyEndpointID:
type: string
readOnly: true
@ -8831,10 +8832,7 @@ components:
format: date-time
readOnly: true
status:
description: The status of the notification rule task.
default: active
type: string
enum: ["active", "inactive"]
$ref: "#/components/schemas/TaskStatusType"
name:
description: human-readable name describing the notification rule
type: string
@ -8864,6 +8862,9 @@ components:
type: array
items:
$ref: "#/components/schemas/TagRule"
description:
description: An optional description of the notification rule
type: string
statusRules:
description: list of status rules the notification rule attempts to match
type: array
@ -8970,6 +8971,9 @@ components:
type: string
format: date-time
readOnly: true
description:
description: An optional description of the notification endpoint
type: string
name:
type: string
status:

View File

@ -157,7 +157,7 @@
"dependencies": {
"@influxdata/clockface": "0.0.13",
"@influxdata/giraffe": "0.16.0",
"@influxdata/influx": "0.5.0",
"@influxdata/influx": "0.5.5",
"@influxdata/influxdb-templates": "0.3.0",
"@influxdata/react-custom-scrollbars": "4.3.8",
"axios": "^0.19.0",

View File

@ -17,28 +17,28 @@ import {Check, GetState} from 'src/types'
export type Action =
| ReturnType<typeof setAllChecks>
| ReturnType<typeof setChecksStatus>
| ReturnType<typeof setCheck>
| ReturnType<typeof setCheckStatus>
| ReturnType<typeof removeCheck>
| ReturnType<typeof setCurrentCheck>
const setAllChecks = (status: RemoteDataState, checks?: Check[]) => ({
export const setAllChecks = (status: RemoteDataState, checks?: Check[]) => ({
type: 'SET_ALL_CHECKS' as 'SET_ALL_CHECKS',
payload: {status, checks},
})
const setChecksStatus = (status: RemoteDataState) => ({
type: 'SET_CHECKS_STATUS' as 'SET_CHECKS_STATUS',
payload: {status},
})
const setCheck = (status: RemoteDataState, check?: Check) => ({
export const setCheck = (check: Check) => ({
type: 'SET_CHECK' as 'SET_CHECK',
payload: {status, check},
payload: {check},
})
const setCheckStatus = (status: RemoteDataState) => ({
type: 'SET_CHECK_STATUS' as 'SET_CHECK_STATUS',
payload: {status},
export const removeCheck = (checkID: string) => ({
type: 'REMOVE_CHECK' as 'REMOVE_CHECK',
payload: {checkID},
})
export const setCurrentCheck = (status: RemoteDataState, check?: Check) => ({
type: 'SET_CURRENT_CHECK' as 'SET_CURRENT_CHECK',
payload: {status, check},
})
export const getChecks = () => async (
@ -46,7 +46,7 @@ export const getChecks = () => async (
getState: GetState
) => {
try {
dispatch(setChecksStatus(RemoteDataState.Loading))
dispatch(setAllChecks(RemoteDataState.Loading))
const {
orgs: {
org: {id: orgID},
@ -58,23 +58,58 @@ export const getChecks = () => async (
dispatch(setAllChecks(RemoteDataState.Done, checks))
} catch (e) {
console.error(e)
dispatch(setChecksStatus(RemoteDataState.Error))
dispatch(setAllChecks(RemoteDataState.Error))
dispatch(notify(copy.getChecksFailed(e.message)))
}
}
export const getCheck = (checkID: string) => async (
export const getCurrentCheck = (checkID: string) => async (
dispatch: Dispatch<Action | NotificationAction>
) => {
try {
dispatch(setCheckStatus(RemoteDataState.Loading))
dispatch(setCurrentCheck(RemoteDataState.Loading))
const check = await client.checks.get(checkID)
dispatch(setCheck(RemoteDataState.Done, check))
dispatch(setCurrentCheck(RemoteDataState.Done, check))
} catch (e) {
console.error(e)
dispatch(setCheckStatus(RemoteDataState.Error))
dispatch(setCurrentCheck(RemoteDataState.Error))
dispatch(notify(copy.getCheckFailed(e.message)))
}
}
export const createCheck = (check: Check) => async (
dispatch: Dispatch<Action | NotificationAction>
) => {
try {
client.checks.create(check)
} catch (e) {
console.error(e)
dispatch(notify(copy.createCheckFailed(e.message)))
}
}
export const updateCheck = (check: Partial<Check>) => async (
dispatch: Dispatch<Action | NotificationAction>
) => {
try {
const updatedCheck = await client.checks.update(check.id, check)
dispatch(setCheck(updatedCheck))
} catch (e) {
console.error(e)
dispatch(notify(copy.updateCheckFailed(e.message)))
}
}
export const deleteCheck = (checkID: string) => async (
dispatch: Dispatch<Action | NotificationAction>
) => {
try {
await client.checks.delete(checkID)
dispatch(removeCheck(checkID))
} catch (e) {
console.error(e)
dispatch(notify(copy.deleteCheckFailed(e.message)))
}
}

View File

@ -5,7 +5,7 @@ import {Dispatch} from 'react'
// Constants
import * as copy from 'src/shared/copy/notifications'
//Actions
// Actions
import {
notify,
Action as NotificationAction,
@ -17,34 +17,34 @@ import {NotificationRule, GetState} from 'src/types'
export type Action =
| ReturnType<typeof setAllNotificationRules>
| ReturnType<typeof setNotificationRulesStatus>
| ReturnType<typeof setNotificationRule>
| ReturnType<typeof setNotificationRuleStatus>
| ReturnType<typeof setCurrentNotificationRule>
| ReturnType<typeof removeNotificationRule>
const setAllNotificationRules = (
export const setAllNotificationRules = (
status: RemoteDataState,
notificationRules?: NotificationRule[]
) => ({
type: 'SET_ALL_NOTIFICATIONRULES' as 'SET_ALL_NOTIFICATIONRULES',
type: 'SET_ALL_NOTIFICATION_RULES' as 'SET_ALL_NOTIFICATION_RULES',
payload: {status, notificationRules},
})
const setNotificationRulesStatus = (status: RemoteDataState) => ({
type: 'SET_NOTIFICATIONRULES_STATUS' as 'SET_NOTIFICATIONRULES_STATUS',
payload: {status},
export const setNotificationRule = (notificationRule: NotificationRule) => ({
type: 'SET_NOTIFICATION_RULE' as 'SET_NOTIFICATION_RULE',
payload: {notificationRule},
})
const setNotificationRule = (
export const setCurrentNotificationRule = (
status: RemoteDataState,
notificationRule?: NotificationRule
) => ({
type: 'SET_NOTIFICATIONRULE' as 'SET_NOTIFICATIONRULE',
type: 'SET_CURRENT_NOTIFICATION_RULE' as 'SET_CURRENT_NOTIFICATION_RULE',
payload: {status, notificationRule},
})
const setNotificationRuleStatus = (status: RemoteDataState) => ({
type: 'SET_NOTIFICATIONRULE_STATUS' as 'SET_NOTIFICATIONRULE_STATUS',
payload: {status},
export const removeNotificationRule = (notificationRuleID: string) => ({
type: 'REMOVE_NOTIFICATION_RULE' as 'REMOVE_NOTIFICATION_RULE',
payload: {notificationRuleID},
})
export const getNotificationRules = () => async (
@ -52,37 +52,77 @@ export const getNotificationRules = () => async (
getState: GetState
) => {
try {
dispatch(setNotificationRulesStatus(RemoteDataState.Loading))
dispatch(setAllNotificationRules(RemoteDataState.Loading))
const {
orgs: {
org: {id: orgID},
},
} = getState()
const notificationRules = await client.notificationRules.getAll(orgID)
const notificationRules = (await client.notificationRules.getAll(
orgID
)) as NotificationRule[]
dispatch(setAllNotificationRules(RemoteDataState.Done, notificationRules))
} catch (e) {
console.error(e)
dispatch(setNotificationRulesStatus(RemoteDataState.Error))
dispatch(setAllNotificationRules(RemoteDataState.Error))
dispatch(notify(copy.getNotificationRulesFailed(e.message)))
}
}
export const getNotificationRule = (notificationRuleID: string) => async (
dispatch: Dispatch<Action | NotificationAction>
) => {
export const getCurrentNotificationRule = (
notificationRuleID: string
) => async (dispatch: Dispatch<Action | NotificationAction>) => {
try {
dispatch(setNotificationRuleStatus(RemoteDataState.Loading))
dispatch(setCurrentNotificationRule(RemoteDataState.Loading))
const notificationRule = await client.notificationRules.get(
const notificationRule = (await client.notificationRules.get(
notificationRuleID
)
)) as NotificationRule
dispatch(setNotificationRule(RemoteDataState.Done, notificationRule))
dispatch(setCurrentNotificationRule(RemoteDataState.Done, notificationRule))
} catch (e) {
console.error(e)
dispatch(setNotificationRuleStatus(RemoteDataState.Error))
dispatch(setCurrentNotificationRule(RemoteDataState.Error))
dispatch(notify(copy.getNotificationRuleFailed(e.message)))
}
}
export const createNotificationRule = (
notificationRule: NotificationRule
) => async (dispatch: Dispatch<Action | NotificationAction>) => {
try {
client.notificationRules.create(notificationRule)
} catch (e) {
console.error(e)
dispatch(notify(copy.createNotificationRuleFailed(e.message)))
}
}
export const updateNotificationRule = (
notificationRule: Partial<NotificationRule>
) => async (dispatch: Dispatch<Action | NotificationAction>) => {
try {
const updatedNotificationRule = (await client.notificationRules.update(
notificationRule.id,
notificationRule
)) as NotificationRule
dispatch(setNotificationRule(updatedNotificationRule))
} catch (e) {
console.error(e)
dispatch(notify(copy.updateNotificationRuleFailed(e.message)))
}
}
export const deleteNotificationRule = (notificationRuleID: string) => async (
dispatch: Dispatch<Action | NotificationAction>
) => {
try {
await client.notificationRules.delete(notificationRuleID)
dispatch(removeNotificationRule(notificationRuleID))
} catch (e) {
console.error(e)
dispatch(notify(copy.deleteNotificationRuleFailed(e.message)))
}
}

View File

@ -0,0 +1,108 @@
// Libraries
import React, {FunctionComponent} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
// Components
import {ResourceList} from 'src/clockface'
import {SlideToggle, ComponentSize} from '@influxdata/clockface'
import CheckCardContext from 'src/alerting/components/CheckCardContext'
// Constants
import {DEFAULT_CHECK_NAME} from 'src/alerting/constants'
// Actions
import {updateCheck, deleteCheck} from 'src/alerting/actions/checks'
// Types
import {Check, CheckBase} from 'src/types'
interface DispatchProps {
updateCheck: typeof updateCheck
deleteCheck: typeof deleteCheck
}
interface OwnProps {
check: Check
}
type Props = OwnProps & DispatchProps & WithRouterProps
const CheckCard: FunctionComponent<Props> = ({
check,
updateCheck,
deleteCheck,
router,
params: {orgID},
}) => {
const onUpdateName = (name: string) => {
updateCheck({id: check.id, name})
}
const onClickName = () => {
router.push(`/orgs/${orgID}/checks/${check.id}`)
}
const onDelete = () => {
deleteCheck(check.id)
}
const onExport = () => {}
const onClone = () => {}
const onToggle = () => {
const status =
check.status == CheckBase.StatusEnum.Active
? CheckBase.StatusEnum.Inactive
: CheckBase.StatusEnum.Active
updateCheck({id: check.id, status})
}
return (
<ResourceList.Card
key={`check-id--${check.id}`}
testID="check-card"
name={() => (
<ResourceList.EditableName
onUpdate={onUpdateName}
onClick={onClickName}
name={check.name}
noNameString={DEFAULT_CHECK_NAME}
parentTestID="check-card--name"
buttonTestID="check-card--name-button"
inputTestID="check-card--input"
/>
)}
toggle={() => (
<SlideToggle
active={check.status == CheckBase.StatusEnum.Active}
size={ComponentSize.ExtraSmall}
onChange={onToggle}
testID="check-card--slide-toggle"
/>
)}
// description
// labels
disabled={check.status == CheckBase.StatusEnum.Inactive}
contextMenu={() => (
<CheckCardContext
onDelete={onDelete}
onExport={onExport}
onClone={onClone}
/>
)}
updatedAt={check.updatedAt.toString()}
/>
)
}
const mdtp: DispatchProps = {
updateCheck: updateCheck,
deleteCheck: deleteCheck,
}
export default connect<{}, DispatchProps, {}>(
null,
mdtp
)(withRouter(CheckCard))

View File

@ -0,0 +1,42 @@
// Libraries
import React, {FunctionComponent} from 'react'
// Components
import {Context, IconFont} from 'src/clockface'
import {ComponentColor} from '@influxdata/clockface'
interface Props {
onDelete: () => void
onClone: () => void
onExport: () => void
}
const CheckCardContext: FunctionComponent<Props> = ({
onDelete,
onClone,
onExport,
}) => {
return (
<Context>
<Context.Menu icon={IconFont.CogThick}>
<Context.Item label="Export" action={onExport} />
</Context.Menu>
<Context.Menu icon={IconFont.Duplicate} color={ComponentColor.Secondary}>
<Context.Item label="Clone" action={onClone} />
</Context.Menu>
<Context.Menu
icon={IconFont.Trash}
color={ComponentColor.Danger}
testID="context-delete-menu"
>
<Context.Item
label="Delete"
action={onDelete}
testID="context-delete-task"
/>
</Context.Menu>
</Context>
)
}
export default CheckCardContext

View File

@ -1,5 +1,10 @@
//Libraries
import React, {FunctionComponent} from 'react'
//Components
import CheckCard from 'src/alerting/components/CheckCard'
import {ResourceList} from 'src/clockface'
// Types
import {Check} from 'src/types'
import {EmptyState, ComponentSize} from '@influxdata/clockface'
@ -9,9 +14,20 @@ interface Props {
}
const CheckCards: FunctionComponent<Props> = ({checks}) => {
if (checks && checks.length) {
return <></>
}
return (
<>
<ResourceList>
<ResourceList.Body emptyState={<EmptyChecksList />}>
{checks.map(check => (
<CheckCard key={check.id} check={check} />
))}
</ResourceList.Body>
</ResourceList>
</>
)
}
const EmptyChecksList: FunctionComponent = () => {
return (
<EmptyState size={ComponentSize.ExtraSmall}>
<EmptyState.Text

View File

@ -0,0 +1,115 @@
// Libraries
import React, {FunctionComponent} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
// Components
import {ResourceList} from 'src/clockface'
import {SlideToggle, ComponentSize} from '@influxdata/clockface'
import NotificationRuleCardContext from 'src/alerting/components/NotificationRuleCardContext'
// Constants
import {DEFAULT_NOTIFICATION_RULE_NAME} from 'src/alerting/constants'
// Actions
import {
updateNotificationRule,
deleteNotificationRule,
} from 'src/alerting/actions/notificationRules'
// Types
import {NotificationRule, NotificationRuleBase} from 'src/types'
interface DispatchProps {
updateNotificationRule: typeof updateNotificationRule
deleteNotificationRule: typeof deleteNotificationRule
}
interface OwnProps {
notificationRule: NotificationRule
}
type Props = OwnProps & DispatchProps & WithRouterProps
const NotificationRuleCard: FunctionComponent<Props> = ({
notificationRule,
updateNotificationRule,
deleteNotificationRule,
router,
params: {orgID},
}) => {
const onUpdateName = (name: string) => {
updateNotificationRule({id: notificationRule.id, name})
}
const onClickName = () => {
router.push(`/orgs/${orgID}/notificationRules/${notificationRule.id}`)
}
const onDelete = () => {
deleteNotificationRule(notificationRule.id)
}
const onExport = () => {}
const onClone = () => {}
const onToggle = () => {
const status =
notificationRule.status == NotificationRuleBase.StatusEnum.Active
? NotificationRuleBase.StatusEnum.Inactive
: NotificationRuleBase.StatusEnum.Active
updateNotificationRule({id: notificationRule.id, status})
}
return (
<ResourceList.Card
key={`notificationRule-id--${notificationRule.id}`}
testID="notificationRule-card"
name={() => (
<ResourceList.EditableName
onUpdate={onUpdateName}
onClick={onClickName}
name={notificationRule.name}
noNameString={DEFAULT_NOTIFICATION_RULE_NAME}
parentTestID="notificationRule-card--name"
buttonTestID="notificationRule-card--name-button"
inputTestID="notificationRule-card--input"
/>
)}
toggle={() => (
<SlideToggle
active={
notificationRule.status == NotificationRuleBase.StatusEnum.Active
}
size={ComponentSize.ExtraSmall}
onChange={onToggle}
testID="notificationRule-card--slide-toggle"
/>
)}
// description
// labels
disabled={
notificationRule.status == NotificationRuleBase.StatusEnum.Inactive
}
contextMenu={() => (
<NotificationRuleCardContext
onDelete={onDelete}
onExport={onExport}
onClone={onClone}
/>
)}
updatedAt={notificationRule.updatedAt.toString()}
/>
)
}
const mdtp: DispatchProps = {
updateNotificationRule: updateNotificationRule,
deleteNotificationRule: deleteNotificationRule,
}
export default connect<{}, DispatchProps, {}>(
null,
mdtp
)(withRouter(NotificationRuleCard))

View File

@ -0,0 +1,42 @@
// Libraries
import React, {FunctionComponent} from 'react'
// Components
import {Context, IconFont} from 'src/clockface'
import {ComponentColor} from '@influxdata/clockface'
interface Props {
onDelete: () => void
onClone: () => void
onExport: () => void
}
const NotificationRuleCardContext: FunctionComponent<Props> = ({
onDelete,
onClone,
onExport,
}) => {
return (
<Context>
<Context.Menu icon={IconFont.CogThick}>
<Context.Item label="Export" action={onExport} />
</Context.Menu>
<Context.Menu icon={IconFont.Duplicate} color={ComponentColor.Secondary}>
<Context.Item label="Clone" action={onClone} />
</Context.Menu>
<Context.Menu
icon={IconFont.Trash}
color={ComponentColor.Danger}
testID="context-delete-menu"
>
<Context.Item
label="Delete"
action={onDelete}
testID="context-delete-task"
/>
</Context.Menu>
</Context>
)
}
export default NotificationRuleCardContext

View File

@ -0,0 +1,43 @@
// Libraries
import React, {FunctionComponent} from 'react'
// Components
import NotificationRuleCard from 'src/alerting/components/NotificationRuleCard'
import {ResourceList} from 'src/clockface'
// Types
import {NotificationRule} from 'src/types'
import {EmptyState, ComponentSize} from '@influxdata/clockface'
interface Props {
notificationRules: NotificationRule[]
}
const NotificationRuleCards: FunctionComponent<Props> = ({
notificationRules,
}) => {
return (
<>
<ResourceList>
<ResourceList.Body emptyState={<EmptyNotificationRulesList />}>
{notificationRules.map(nr => (
<NotificationRuleCard key={nr.id} notificationRule={nr} />
))}
</ResourceList.Body>
</ResourceList>
</>
)
}
const EmptyNotificationRulesList: FunctionComponent = () => {
return (
<EmptyState size={ComponentSize.ExtraSmall}>
<EmptyState.Text
text="Looks like you dont have any Notification Rules, why not create one?"
highlightWords={['Notification Rules']}
/>
</EmptyState>
)
}
export default NotificationRuleCards

View File

@ -4,6 +4,7 @@ import {connect} from 'react-redux'
// Types
import {NotificationRule, AppState} from 'src/types'
import NotificationRuleCards from 'src/alerting/components/NotificationRuleCards'
interface StateProps {
notificationRules: NotificationRule[]
@ -11,8 +12,15 @@ interface StateProps {
type Props = StateProps
const NotificationRulesColumn: FunctionComponent<Props> = () => {
return <>NotificationRules</>
const NotificationRulesColumn: FunctionComponent<Props> = ({
notificationRules,
}) => {
return (
<>
NotificationRules
<NotificationRuleCards notificationRules={notificationRules} />
</>
)
}
const mstp = (state: AppState) => {

View File

@ -0,0 +1,85 @@
import {
Check,
CheckType,
DashboardQuery,
QueryEditMode,
CheckBase,
NotificationRule,
NotificationRuleBase,
NotificationRuleType,
CheckStatusLevel,
ThresholdType,
} from 'src/types'
export const DEFAULT_CHECK_NAME = 'Name this check'
export const DEFAULT_NOTIFICATION_RULE_NAME = 'Name this notification rule'
export const query: DashboardQuery = {
text: 'this is query',
editMode: QueryEditMode.Advanced,
builderConfig: null,
name: 'great q',
}
export const check1: Check = {
id: '1',
type: CheckType.Threshold,
name: 'Amoozing check',
orgID: 'lala',
createdAt: new Date('December 17, 2019'),
updatedAt: new Date('April 17, 2019'),
query: query,
status: CheckBase.StatusEnum.Active,
every: '2d',
offset: '1m',
tags: [{key: 'a', value: 'b'}],
statusMessageTemplate: 'this is a great message template',
thresholds: [
{
level: CheckStatusLevel.WARN,
allValues: false,
type: ThresholdType.Greater,
},
],
}
export const check2: Check = {
id: '2',
type: CheckType.Threshold,
name: 'Another check',
orgID: 'lala',
createdAt: new Date('December 17, 2019'),
updatedAt: new Date('April 17, 2019'),
query: query,
status: CheckBase.StatusEnum.Active,
every: '2d',
offset: '1m',
tags: [{key: 'a', value: 'b'}],
statusMessageTemplate: 'this is a great message template',
thresholds: [
{
level: CheckStatusLevel.WARN,
allValues: false,
type: ThresholdType.Greater,
},
],
}
export const checks: Array<Check> = [check1, check2]
export const notificationRule: NotificationRule = {
id: '3',
notifyEndpointID: '2',
orgID: 'lala',
createdAt: new Date('December 17, 2019'),
updatedAt: new Date('April 17, 2019'),
status: NotificationRuleBase.StatusEnum.Active,
name: 'amazing notification rule',
type: NotificationRuleType.Slack,
every: '2d',
offset: '5m',
limitEvery: 1,
limit: 5,
tagRules: [],
statusRules: [],
}

View File

@ -0,0 +1,90 @@
import checksReducer, {defaultChecksState} from 'src/alerting/reducers/checks'
import {
setAllChecks,
setCheck,
setCurrentCheck,
removeCheck,
} from 'src/alerting/actions/checks'
import {RemoteDataState} from 'src/types'
import {check1, check2} from 'src/alerting/constants'
describe('checksReducer', () => {
describe('setAllChecks', () => {
it('sets list and status properties of state.', () => {
const initialState = defaultChecksState
const actual = checksReducer(
initialState,
setAllChecks(RemoteDataState.Done, [check1, check2])
)
const expected = {
...defaultChecksState,
list: [check1, check2],
status: RemoteDataState.Done,
}
expect(actual).toEqual(expected)
})
})
describe('setCheck', () => {
it('adds check to list if it is new', () => {
const initialState = defaultChecksState
const actual = checksReducer(initialState, setCheck(check2))
const expected = {
...defaultChecksState,
list: [check2],
}
expect(actual).toEqual(expected)
})
it('updates check in list if it exists', () => {
let initialState = defaultChecksState
initialState.list = [check1]
const actual = checksReducer(
initialState,
setCheck({...check1, name: check2.name})
)
const expected = {
...defaultChecksState,
list: [{...check1, name: check2.name}],
}
expect(actual).toEqual(expected)
})
})
describe('removeCheck', () => {
it('removes check from list', () => {
const initialState = defaultChecksState
initialState.list = [check1]
const actual = checksReducer(initialState, removeCheck(check1.id))
const expected = {
...defaultChecksState,
list: [],
}
expect(actual).toEqual(expected)
})
})
describe('setCurrentCheck', () => {
it('sets current check and status.', () => {
const initialState = defaultChecksState
const actual = checksReducer(
initialState,
setCurrentCheck(RemoteDataState.Done, check1)
)
const expected = {
...defaultChecksState,
current: {status: RemoteDataState.Done, check: check1},
}
expect(actual).toEqual(expected)
})
})
})

View File

@ -1,3 +1,6 @@
// Libraries
import {produce} from 'immer'
// Types
import {RemoteDataState, Check} from 'src/types'
import {Action} from 'src/alerting/actions/checks'
@ -17,31 +20,38 @@ export const defaultChecksState: ChecksState = {
export default (
state: ChecksState = defaultChecksState,
action: Action
): ChecksState => {
switch (action.type) {
case 'SET_CHECKS_STATUS':
return {
...state,
status: action.payload.status,
}
case 'SET_ALL_CHECKS':
return {
...state,
list: action.payload.checks,
status: RemoteDataState.Done,
}
case 'SET_CHECK_STATUS':
return {
...state,
current: {...state.current, status: action.payload.status},
}
case 'SET_CHECK':
return {
...state,
current: {status: action.payload.status, check: action.payload.check},
}
): ChecksState =>
produce(state, draftState => {
switch (action.type) {
case 'SET_ALL_CHECKS':
const {status, checks} = action.payload
draftState.status = status
if (checks) {
draftState.list = checks
}
return
default:
return state
}
}
case 'SET_CHECK':
const newCheck = action.payload.check
const checkIndex = state.list.findIndex(c => c.id == newCheck.id)
if (checkIndex == -1) {
draftState.list.push(newCheck)
} else {
draftState.list[checkIndex] = newCheck
}
return
case 'REMOVE_CHECK':
const {checkID} = action.payload
draftState.list = draftState.list.filter(c => c.id != checkID)
return
case 'SET_CURRENT_CHECK':
draftState.current.status = action.payload.status
if (action.payload.check) {
draftState.current.check = action.payload.check
}
return
}
})

View File

@ -1,3 +1,6 @@
// Libraries
import {produce} from 'immer'
// Types
import {RemoteDataState, NotificationRule} from 'src/types'
import {Action} from 'src/alerting/actions/notificationRules'
@ -8,43 +11,51 @@ export interface NotificationRulesState {
current: {status: RemoteDataState; notificationRule: NotificationRule}
}
export const defaultNotificationRuleState: NotificationRulesState = {
export const defaultNotificationRulesState: NotificationRulesState = {
status: RemoteDataState.NotStarted,
list: [],
current: {status: RemoteDataState.NotStarted, notificationRule: null},
}
export default (
state: NotificationRulesState = defaultNotificationRuleState,
state: NotificationRulesState = defaultNotificationRulesState,
action: Action
): NotificationRulesState => {
switch (action.type) {
case 'SET_NOTIFICATIONRULES_STATUS':
return {
...state,
status: action.payload.status,
}
case 'SET_ALL_NOTIFICATIONRULES':
return {
...state,
list: action.payload.notificationRules,
status: RemoteDataState.Done,
}
case 'SET_NOTIFICATIONRULE_STATUS':
return {
...state,
current: {...state.current, status: action.payload.status},
}
case 'SET_NOTIFICATIONRULE':
return {
...state,
current: {
status: action.payload.status,
notificationRule: action.payload.notificationRule,
},
}
): NotificationRulesState =>
produce(state, draftState => {
switch (action.type) {
case 'SET_ALL_NOTIFICATION_RULES':
const {status, notificationRules} = action.payload
draftState.status = status
if (notificationRules) {
draftState.list = notificationRules
}
return
default:
return state
}
}
case 'SET_NOTIFICATION_RULE':
const newNotificationRule = action.payload.notificationRule
const notificationRuleIndex = state.list.findIndex(
nr => nr.id == newNotificationRule.id
)
if (notificationRuleIndex == -1) {
draftState.list.push(newNotificationRule)
} else {
draftState.list[notificationRuleIndex] = newNotificationRule
}
return
case 'REMOVE_NOTIFICATION_RULE':
const {notificationRuleID} = action.payload
draftState.list = draftState.list.filter(
nr => nr.id != notificationRuleID
)
return
case 'SET_CURRENT_NOTIFICATION_RULE':
draftState.current.status = action.payload.status
if (action.payload.notificationRule) {
draftState.current.notificationRule = action.payload.notificationRule
}
return
}
})

View File

@ -0,0 +1,105 @@
import notificationRulesReducer, {
defaultNotificationRulesState,
} from 'src/alerting/reducers/notificationRules'
import {
setAllNotificationRules,
setNotificationRule,
setCurrentNotificationRule,
removeNotificationRule,
} from 'src/alerting/actions/notificationRules'
import {RemoteDataState} from 'src/types'
import {notificationRule} from 'src/alerting/constants'
describe('notificationRulesReducer', () => {
describe('setAllNotificationRules', () => {
it('sets list and status properties of state.', () => {
const initialState = defaultNotificationRulesState
const actual = notificationRulesReducer(
initialState,
setAllNotificationRules(RemoteDataState.Done, [notificationRule])
)
const expected = {
...defaultNotificationRulesState,
list: [notificationRule],
status: RemoteDataState.Done,
}
expect(actual).toEqual(expected)
})
})
describe('setNotificationRule', () => {
it('adds notificationRule to list if it is new', () => {
const initialState = defaultNotificationRulesState
const actual = notificationRulesReducer(
initialState,
setNotificationRule(notificationRule)
)
const expected = {
...defaultNotificationRulesState,
list: [notificationRule],
}
expect(actual).toEqual(expected)
})
it('updates notificationRule in list if it exists', () => {
let initialState = defaultNotificationRulesState
initialState.list = [notificationRule]
const actual = notificationRulesReducer(
initialState,
setNotificationRule({
...notificationRule,
name: 'moo',
})
)
const expected = {
...defaultNotificationRulesState,
list: [{...notificationRule, name: 'moo'}],
}
expect(actual).toEqual(expected)
})
})
describe('removeNotificationRule', () => {
it('removes notificationRule from list', () => {
const initialState = defaultNotificationRulesState
initialState.list = [notificationRule]
const actual = notificationRulesReducer(
initialState,
removeNotificationRule(notificationRule.id)
)
const expected = {
...defaultNotificationRulesState,
list: [],
}
expect(actual).toEqual(expected)
})
})
describe('setCurrentNotificationRule', () => {
it('sets current notificationRule and status.', () => {
const initialState = defaultNotificationRulesState
const actual = notificationRulesReducer(
initialState,
setCurrentNotificationRule(RemoteDataState.Done, notificationRule)
)
const expected = {
...defaultNotificationRulesState,
current: {
status: RemoteDataState.Done,
notificationRule: notificationRule,
},
}
expect(actual).toEqual(expected)
})
})
})

View File

@ -29,7 +29,7 @@ interface Props {
onAddScraper: () => void
}
export default class MemberContextMenu extends PureComponent<Props> {
export default class BucketContextMenu extends PureComponent<Props> {
public render() {
const {
bucket,

View File

@ -17,16 +17,23 @@ import {VIS_THEME} from 'src/shared/constants'
import {INVALID_DATA_COPY} from 'src/shared/copy/cell'
// Types
import {RemoteDataState, CheckView, TimeZone, ThresholdConfig} from 'src/types'
import {
RemoteDataState,
CheckView,
TimeZone,
CheckThreshold,
ThresholdType,
CheckStatusLevel,
} from 'src/types'
const X_COLUMN = '_time'
const Y_COLUMN = '_value'
const THRESHOLDS: ThresholdConfig[] = [
const THRESHOLDS: CheckThreshold[] = [
{
type: 'less',
type: ThresholdType.Greater,
allValues: false,
level: 'UNKNOWN',
level: CheckStatusLevel.UNKNOWN,
value: 20,
},
]
@ -46,12 +53,11 @@ const CheckPlot: FunctionComponent<Props> = ({
loading,
children,
timeZone,
viewProperties: {yDomain: storedYDomain},
}) => {
const [thresholds, setThresholds] = useState(THRESHOLDS)
const [yDomain, onSetYDomain, onResetYDomain] = useVisDomainSettings(
storedYDomain,
[0, 100],
table.getColumn(Y_COLUMN, 'number')
)

View File

@ -11,12 +11,12 @@ import {isInDomain, clamp} from 'src/shared/utils/vis'
import {DragEvent} from 'src/shared/utils/useDragBehavior'
// Types
import {GreaterThresholdConfig} from 'src/types'
import {GreaterThreshold} from 'src/types'
interface Props {
yScale: Scale<number, number>
yDomain: number[]
threshold: GreaterThresholdConfig
threshold: GreaterThreshold
onChangePos: (e: DragEvent) => void
}

View File

@ -11,12 +11,12 @@ import {clamp, isInDomain} from 'src/shared/utils/vis'
import {DragEvent} from 'src/shared/utils/useDragBehavior'
// Types
import {LessThresholdConfig} from 'src/types'
import {LesserThreshold} from 'src/types'
interface Props {
yScale: Scale<number, number>
yDomain: number[]
threshold: LessThresholdConfig
threshold: LesserThreshold
onChangePos: (e: DragEvent) => void
}

View File

@ -11,12 +11,12 @@ import {isInDomain, clamp} from 'src/shared/utils/vis'
import {DragEvent} from 'src/shared/utils/useDragBehavior'
// Types
import {RangeThresholdConfig} from 'src/types'
import {RangeThreshold} from 'src/types'
interface Props {
yScale: Scale<number, number>
yDomain: number[]
threshold: RangeThresholdConfig
threshold: RangeThreshold
onChangeMaxPos: (e: DragEvent) => void
onChangeMinPos: (e: DragEvent) => void
}
@ -24,33 +24,33 @@ interface Props {
const RangeThresholdMarkers: FunctionComponent<Props> = ({
yScale,
yDomain,
threshold: {level, within, minValue, maxValue},
threshold: {level, within, min, max},
onChangeMinPos,
onChangeMaxPos,
}) => {
const minY = yScale(clamp(minValue, yDomain))
const maxY = yScale(clamp(maxValue, yDomain))
const minY = yScale(clamp(min, yDomain))
const maxY = yScale(clamp(max, yDomain))
return (
<>
{isInDomain(minValue, yDomain) && (
{isInDomain(min, yDomain) && (
<ThresholdMarker level={level} y={minY} onDrag={onChangeMinPos} />
)}
{isInDomain(maxValue, yDomain) && (
{isInDomain(max, yDomain) && (
<ThresholdMarker level={level} y={maxY} onDrag={onChangeMaxPos} />
)}
{within ? (
<ThresholdMarkerArea level={level} top={maxY} height={minY - maxY} />
) : (
<>
{maxValue <= yDomain[1] && (
{max <= yDomain[1] && (
<ThresholdMarkerArea
level={level}
top={yScale(yDomain[1])}
height={maxY - yScale(yDomain[1])}
/>
)}
{minValue >= yDomain[0] && (
{min >= yDomain[0] && (
<ThresholdMarkerArea
level={level}
top={minY}

View File

@ -24,12 +24,13 @@ import {
VariableAssignment,
QueryViewProperties,
ViewType,
CheckViewProperties,
} from 'src/types'
interface OwnProps {
timeRange: TimeRange
manualRefresh: number
properties: QueryViewProperties
properties: QueryViewProperties | CheckViewProperties
dashboardID: string
}

View File

@ -11,11 +11,11 @@ import GreaterThresholdMarker from 'src/shared/components/GreaterThresholdMarker
import {clamp} from 'src/shared/utils/vis'
// Types
import {ThresholdConfig} from 'src/types'
import {CheckThreshold, ThresholdType} from 'src/types'
interface Props {
thresholds: ThresholdConfig[]
onSetThresholds: (newThresholds: ThresholdConfig[]) => void
thresholds: CheckThreshold[]
onSetThresholds: (newThresholds: CheckThreshold[]) => void
yScale: Scale<number, number>
yDomain: number[]
}
@ -31,18 +31,21 @@ const ThresholdMarkers: FunctionComponent<Props> = ({
const handleDrag = (index: number, field: string, y: number) => {
const yRelative = y - originRef.current.getBoundingClientRect().top
const yValue = clamp(yScale.invert(yRelative), yDomain)
const nextThreshold = {...thresholds[index], [field]: yValue}
const nextThreshold: CheckThreshold = {
...thresholds[index],
[field]: yValue,
}
if (
nextThreshold.type === 'range' &&
nextThreshold.minValue > nextThreshold.maxValue
nextThreshold.type === ThresholdType.Range &&
nextThreshold.min > nextThreshold.max
) {
// If the user drags the min past the max or vice versa, we swap the
// values that are set so that the min is always at most the max
const maxValue = nextThreshold.minValue
const maxValue = nextThreshold.min
nextThreshold.minValue = nextThreshold.maxValue
nextThreshold.maxValue = maxValue
nextThreshold.min = nextThreshold.max
nextThreshold.max = maxValue
}
const nextThresholds = thresholds.map((t, i) =>
@ -60,7 +63,7 @@ const ThresholdMarkers: FunctionComponent<Props> = ({
const onChangeMinPos = ({y}) => handleDrag(index, 'minValue', y)
switch (threshold.type) {
case 'greater':
case ThresholdType.Greater:
return (
<GreaterThresholdMarker
key={index}
@ -70,7 +73,7 @@ const ThresholdMarkers: FunctionComponent<Props> = ({
onChangePos={onChangePos}
/>
)
case 'less':
case ThresholdType.Lesser:
return (
<LessThresholdMarker
key={index}
@ -80,7 +83,7 @@ const ThresholdMarkers: FunctionComponent<Props> = ({
onChangePos={onChangePos}
/>
)
case 'range':
case ThresholdType.Range:
return (
<RangeThresholdMarkers
key={index}

View File

@ -22,13 +22,14 @@ import {
XYViewGeom,
RemoteDataState,
TimeZone,
CheckViewProperties,
} from 'src/types'
interface Props {
giraffeResult: FromFluxResult
files: string[]
loading: RemoteDataState
properties: QueryViewProperties
properties: QueryViewProperties | CheckViewProperties
timeZone: TimeZone
}

View File

@ -747,3 +747,39 @@ export const getNotificationRuleFailed = (message: string): Notification => ({
...defaultErrorNotification,
message: `Failed to get notification rule: ${message}`,
})
export const createCheckFailed = (message: string): Notification => ({
...defaultErrorNotification,
message: `Failed to create check: ${message}`,
})
export const updateCheckFailed = (message: string): Notification => ({
...defaultErrorNotification,
message: `Failed to update check: ${message}`,
})
export const deleteCheckFailed = (message: string): Notification => ({
...defaultErrorNotification,
message: `Failed to delete check: ${message}`,
})
export const createNotificationRuleFailed = (
message: string
): Notification => ({
...defaultErrorNotification,
message: `Failed to create notification rule: ${message}`,
})
export const updateNotificationRuleFailed = (
message: string
): Notification => ({
...defaultErrorNotification,
message: `Failed to update notification rule: ${message}`,
})
export const deleteNotificationRuleFailed = (
message: string
): Notification => ({
...defaultErrorNotification,
message: `Failed to delete notification rule: ${message}`,
})

View File

@ -28,7 +28,7 @@ import {
QueryView,
QueryViewProperties,
ExtractWorkingView,
AggregateWindow,
BuilderConfigAggregateWindow,
} from 'src/types/dashboards'
import {Action} from 'src/timeMachine/actions'
import {TimeMachineTab} from 'src/types/timeMachine'
@ -39,7 +39,7 @@ interface QueryBuilderState {
buckets: string[]
bucketsStatus: RemoteDataState
functions: Array<[{name: string}]>
aggregateWindow: AggregateWindow
aggregateWindow: BuilderConfigAggregateWindow
tags: Array<{
valuesSearchTerm: string
keysSearchTerm: string

View File

@ -1 +1,40 @@
export {Check, NotificationRule} from '@influxdata/influx'
export {
Check,
CheckType,
CheckBaseTags,
CheckBase,
CheckStatusLevel,
ThresholdCheck,
ThresholdType,
GreaterThreshold,
LesserThreshold,
RangeThreshold,
DeadmanCheck,
NotificationRuleType,
CheckThreshold,
NotificationRuleBase,
} from '@influxdata/influx'
import {
SlackNotificationRule as SlackNotificationRuleAPI,
SMTPNotificationRule as SMTPNotificationRuleAPI,
PagerDutyNotificationRule as PagerDutyNotificationRuleAPI,
} from '@influxdata/influx'
export interface SlackNotificationRule extends SlackNotificationRuleAPI {
id: string
}
export interface SMTPNotificationRule extends SMTPNotificationRuleAPI {
id: string
}
export interface PagerDutyNotificationRule
extends PagerDutyNotificationRuleAPI {
id: string
}
export type NotificationRule =
| SlackNotificationRule
| SMTPNotificationRule
| PagerDutyNotificationRule

View File

@ -5,6 +5,16 @@ import {
IDashboard as DashboardAPI,
Cell as CellAPI,
ViewLinks,
DashboardQuery,
CheckViewProperties,
} from '@influxdata/influx'
export {
CheckView,
CheckViewProperties,
DashboardQuery,
BuilderConfig,
BuilderConfigAggregateWindow,
} from '@influxdata/influx'
export enum Scale {
@ -57,24 +67,6 @@ export enum QueryEditMode {
Advanced = 'advanced',
}
export interface AggregateWindow {
period: string
}
export interface BuilderConfig {
buckets: string[]
tags: Array<{key: string; values: string[]}>
functions: Array<{name: string}>
aggregateWindow: AggregateWindow
}
export interface DashboardQuery {
text: string
editMode: QueryEditMode
builderConfig: BuilderConfig
name: string
}
export interface DashboardDraftQuery extends DashboardQuery {
hidden: boolean
}
@ -129,7 +121,7 @@ export type ViewProperties =
| HistogramView
| HeatmapView
| ScatterView
| CheckView
| CheckViewProperties
export type QueryViewProperties = Extract<
ViewProperties,
@ -290,47 +282,6 @@ export interface ScatterView {
showNoteWhenEmpty: boolean
}
export type CheckStatusLevel = 'OK' | 'INFO' | 'WARN' | 'CRIT' | 'UNKNOWN'
export interface GreaterThresholdConfig {
type: 'greater'
level: CheckStatusLevel
allValues: boolean
value: number
}
export interface LessThresholdConfig {
type: 'less'
level: CheckStatusLevel
allValues: boolean
value: number
}
export interface RangeThresholdConfig {
type: 'range'
level: CheckStatusLevel
allValues: boolean
minValue: number
maxValue: number
within: boolean
}
export type ThresholdConfig =
| GreaterThresholdConfig
| LessThresholdConfig
| RangeThresholdConfig
export interface CheckView {
type: ViewType.Check
shape: ViewShape.ChronografV2
queries: DashboardQuery[]
thresholds: ThresholdConfig[]
yDomain: [number, number]
colors: string[]
note: string
showNoteWhenEmpty: boolean
}
export interface MarkdownView {
type: ViewType.Markdown
shape: ViewShape.ChronografV2
@ -367,12 +318,6 @@ interface DashboardFileMetaSection {
export type NewCell = Omit<Cell, 'id' | 'links' | 'dashboardID'>
export enum ThresholdType {
Text = 'text',
BG = 'background',
Base = 'base',
}
export interface DashboardSwitcherLink {
key: string
text: string

View File

@ -1151,10 +1151,10 @@
resolved "https://registry.yarnpkg.com/@influxdata/giraffe/-/giraffe-0.16.0.tgz#b04a304460a7c9449fe1a9fa2a3f5db97949e916"
integrity sha512-nDVQgx5Lq3fjsMXTzYwzf0HXKjSxzsBJibitObwtE0wefpzU4LW0IEUrcCBNIQyXj1OxyBNL9a67gZ4DyV7M2w==
"@influxdata/influx@0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@influxdata/influx/-/influx-0.5.0.tgz#dedf688d84a50b526a15362633bd5ee7f1e7a95b"
integrity sha512-P24C5j20RRMX2JT43vTdoSozjZp01rcXxOAoIMR+sDMSMG4XuuckIEEOsdTnP1w+N2ivhjC74MjzmswxKeO0fw==
"@influxdata/influx@0.5.5":
version "0.5.5"
resolved "https://registry.yarnpkg.com/@influxdata/influx/-/influx-0.5.5.tgz#ff30862ba3837df8e6e237634e7c844a00c03c08"
integrity sha512-mmyymYIT/HpWDrdR1/x9+ENJIrZTFyr46EF42adqRwujwLxVyG1MPOC7H6aoH87DZgVxKV4nt+zmRSgOnetdpg==
dependencies:
axios "^0.19.0"