Merge pull request #3006 from influxdata/bugfix/task_enabled_correct_check

Fix Kapacitor Rules task enabled checkboxes to only toggle exactly as clicked
pull/10616/head
Jared Scheib 2018-03-23 10:48:41 -07:00 committed by GitHub
commit 0bce2598eb
18 changed files with 1105 additions and 309 deletions

View File

@ -6,6 +6,8 @@
### UI Improvements
1. [#2910](https://github.com/influxdata/chronograf/pull/2910): Redesign system notifications
### Bug Fixes
1. [#2911](https://github.com/influxdata/chronograf/pull/2911): Fix Heroku OAuth
@ -14,11 +16,11 @@
1. [#2866](https://github.com/influxdata/chronograf/pull/2866): Change hover text on delete mappings confirmation button to 'Delete'
1. [#2919](https://github.com/influxdata/chronograf/pull/2919): Automatically add graph type 'line' to any graph missing a type
1. [#2970](https://github.com/influxdata/chronograf/pull/2970): Fix hanging browser on docker host dashboard
1. [#3006](https://github.com/influxdata/chronograf/pull/3006): Fix Kapacitor Rules task enabled checkboxes to only toggle exactly as clicked
## v1.4.2.3 [2018-03-08]
## v1.4.2.2 [2018-03-07]
1. [#2910](https://github.com/influxdata/chronograf/pull/2910): Redesign system notifications
### Bug Fixes

View File

@ -28,7 +28,6 @@ import {
KapacitorPage,
KapacitorRulePage,
KapacitorRulesPage,
KapacitorTasksPage,
TickscriptPage,
} from 'src/kapacitor'
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
@ -154,7 +153,6 @@ const Root = React.createClass({
path="kapacitors/:id/edit:hash"
component={KapacitorPage}
/>
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />
<Route path="admin-chronograf" component={AdminChronografPage} />
<Route path="admin-influxdb" component={AdminInfluxDBPage} />
<Route path="manage-sources" component={ManageSources} />

View File

@ -3,11 +3,8 @@ import PropTypes from 'prop-types'
import {Link} from 'react-router'
import NoKapacitorError from 'shared/components/NoKapacitorError'
import SourceIndicator from 'shared/components/SourceIndicator'
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
import TasksTable from 'src/kapacitor/components/TasksTable'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
const KapacitorRules = ({
source,
@ -19,7 +16,7 @@ const KapacitorRules = ({
}) => {
if (loading) {
return (
<PageContents>
<div>
<div className="panel-heading">
<h2 className="panel-title">Alert Rules</h2>
<button className="btn btn-primary btn-sm disabled" disabled={true}>
@ -31,16 +28,12 @@ const KapacitorRules = ({
<p>Loading Rules...</p>
</div>
</div>
</PageContents>
</div>
)
}
if (!hasKapacitor) {
return (
<PageContents>
<NoKapacitorError source={source} />
</PageContents>
)
return <NoKapacitorError source={source} />
}
const builderRules = rules.filter(r => r.query)
@ -53,7 +46,7 @@ const KapacitorRules = ({
}`
return (
<PageContents source={source}>
<div>
<div className="panel">
<div className="panel-heading">
<h2 className="panel-title">{builderHeader}</h2>
@ -94,37 +87,11 @@ const KapacitorRules = ({
/>
</div>
</div>
</PageContents>
</div>
)
}
const PageContents = ({children}) => (
<div className="page">
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Manage Tasks</h1>
</div>
<div className="page-header__right">
<QuestionMarkTooltip
tipID="manage-tasks--tooltip"
tipContent="<b>Alert Rules</b> generate a TICKscript for<br/>you using our Builder UI.<br/><br/>Not all TICKscripts can be edited<br/>using the Builder."
/>
<SourceIndicator />
</div>
</div>
</div>
<FancyScrollbar className="page-contents fancy-scroll--kapacitor">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">{children}</div>
</div>
</div>
</FancyScrollbar>
</div>
)
const {arrayOf, bool, func, node, shape} = PropTypes
const {arrayOf, bool, func, shape} = PropTypes
KapacitorRules.propTypes = {
source: shape(),
@ -135,9 +102,4 @@ KapacitorRules.propTypes = {
onDelete: func,
}
PageContents.propTypes = {
children: node,
onCloseTickscript: func,
}
export default KapacitorRules

View File

@ -1,104 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {Link} from 'react-router'
import _ from 'lodash'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {parseAlertNodeList} from 'src/shared/parsing/parseHandlersFromRule'
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
const {
colName,
colTrigger,
colMessage,
colAlerts,
colEnabled,
colActions,
} = TASKS_TABLE
const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) => (
<table className="table v-center table-highlight">
<thead>
<tr>
<th style={{minWidth: colName}}>Name</th>
<th style={{width: colTrigger}}>Rule Type</th>
<th style={{width: colMessage}}>Message</th>
<th style={{width: colAlerts}}>Alert Handlers</th>
<th style={{width: colEnabled}} className="text-center">
Task Enabled
</th>
<th style={{width: colActions}} />
</tr>
</thead>
<tbody>
{_.sortBy(rules, r => r.name.toLowerCase()).map(rule => {
return (
<RuleRow
key={rule.id}
rule={rule}
source={source}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
)
})}
</tbody>
</table>
)
const handleDelete = (rule, onDelete) => onDelete(rule)
const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) => (
<tr key={rule.id}>
<td style={{minWidth: colName}}>
<Link to={`/sources/${source.id}/alert-rules/${rule.id}`}>
{rule.name}
</Link>
</td>
<td style={{width: colTrigger, textTransform: 'capitalize'}}>
{rule.trigger}
</td>
<td style={{width: colMessage}}>{rule.message}</td>
<td style={{width: colAlerts}}>{parseAlertNodeList(rule)}</td>
<td style={{width: colEnabled}} className="text-center">
<div className="dark-checkbox">
<input
id={`kapacitor-enabled ${rule.id}`}
className="form-control-static"
type="checkbox"
defaultChecked={rule.status === 'enabled'}
onClick={onChangeRuleStatus(rule)}
/>
<label htmlFor={`kapacitor-enabled ${rule.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right">
<ConfirmButton
text="Delete"
type="btn-danger"
size="btn-xs"
customClass="table--show-on-row-hover"
confirmAction={handleDelete(rule, onDelete)}
/>
</td>
</tr>
)
const {arrayOf, func, shape, string} = PropTypes
KapacitorRulesTable.propTypes = {
rules: arrayOf(shape()),
onChangeRuleStatus: func,
onDelete: func,
source: shape({
id: string.isRequired,
}).isRequired,
}
RuleRow.propTypes = {
rule: shape(),
source: shape(),
onChangeRuleStatus: func,
onDelete: func,
}
export default KapacitorRulesTable

View File

@ -0,0 +1,131 @@
import React, {PureComponent, SFC} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import {AlertRule, Source} from 'src/types'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {parseAlertNodeList} from 'src/shared/parsing/parseHandlersFromRule'
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
const {
colName,
colTrigger,
colMessage,
colAlerts,
colEnabled,
colActions,
} = TASKS_TABLE
interface KapacitorRulesTableProps {
rules: AlertRule[]
source: Source
onChangeRuleStatus: (rule: AlertRule) => void
onDelete: (rule: AlertRule) => void
}
interface RuleRowProps {
rule: AlertRule
source: Source
onChangeRuleStatus: (rule: AlertRule) => void
onDelete: (rule: AlertRule) => void
}
const KapacitorRulesTable: SFC<KapacitorRulesTableProps> = ({
rules,
source,
onChangeRuleStatus,
onDelete,
}) => (
<table className="table v-center table-highlight">
<thead>
<tr>
<th style={{minWidth: colName}}>Name</th>
<th style={{width: colTrigger}}>Rule Type</th>
<th style={{width: colMessage}}>Message</th>
<th style={{width: colAlerts}}>Alert Handlers</th>
<th style={{width: colEnabled}} className="text-center">
Task Enabled
</th>
<th style={{width: colActions}} />
</tr>
</thead>
<tbody>
{_.sortBy(rules, r => r.name.toLowerCase()).map(rule => {
return (
<RuleRow
key={rule.id}
rule={rule}
source={source}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
)
})}
</tbody>
</table>
)
export class RuleRow extends PureComponent<RuleRowProps> {
constructor(props) {
super(props)
this.handleClickRuleStatusEnabled = this.handleClickRuleStatusEnabled.bind(
this
)
this.handleDelete = this.handleDelete.bind(this)
}
public handleClickRuleStatusEnabled(rule: AlertRule) {
return () => {
this.props.onChangeRuleStatus(rule)
}
}
public handleDelete(rule: AlertRule) {
return () => {
this.props.onDelete(rule)
}
}
public render() {
const {rule, source} = this.props
return (
<tr key={rule.id}>
<td style={{minWidth: colName}}>
<Link to={`/sources/${source.id}/alert-rules/${rule.id}`}>
{rule.name}
</Link>
</td>
<td style={{width: colTrigger, textTransform: 'capitalize'}}>
{rule.trigger}
</td>
<td style={{width: colMessage}}>{rule.message}</td>
<td style={{width: colAlerts}}>{parseAlertNodeList(rule)}</td>
<td style={{width: colEnabled}} className="text-center">
<div className="dark-checkbox">
<input
id={`kapacitor-rule-row-task-enabled ${rule.id}`}
className="form-control-static"
type="checkbox"
checked={rule.status === 'enabled'}
onChange={this.handleClickRuleStatusEnabled(rule)}
/>
<label htmlFor={`kapacitor-rule-row-task-enabled ${rule.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right">
<ConfirmButton
text="Delete"
type="btn-danger"
size="btn-xs"
customClass="table--show-on-row-hover"
confirmAction={this.handleDelete(rule)}
/>
</td>
</tr>
)
}
}
export default KapacitorRulesTable

View File

@ -1,94 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {Link} from 'react-router'
import _ from 'lodash'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
const {colName, colType, colEnabled, colActions} = TASKS_TABLE
const TasksTable = ({tasks, source, onDelete, onChangeRuleStatus}) => (
<table className="table v-center table-highlight">
<thead>
<tr>
<th style={{minWidth: colName}}>Name</th>
<th style={{width: colType}}>Type</th>
<th style={{width: colEnabled}} className="text-center">
Task Enabled
</th>
<th style={{width: colActions}} />
</tr>
</thead>
<tbody>
{_.sortBy(tasks, t => t.id.toLowerCase()).map(task => {
return (
<TaskRow
key={task.id}
task={task}
source={source}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
)
})}
</tbody>
</table>
)
const handleDelete = (task, onDelete) => onDelete(task)
const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) => (
<tr key={task.id}>
<td style={{minWidth: colName}}>
<Link
className="link-success"
to={`/sources/${source.id}/tickscript/${task.id}`}
>
{task.name}
</Link>
</td>
<td style={{width: colType, textTransform: 'capitalize'}}>{task.type}</td>
<td style={{width: colEnabled}} className="text-center">
<div className="dark-checkbox">
<input
id={`kapacitor-enabled ${task.id}`}
className="form-control-static"
type="checkbox"
defaultChecked={task.status === 'enabled'}
onClick={onChangeRuleStatus(task)}
/>
<label htmlFor={`kapacitor-enabled ${task.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right">
<ConfirmButton
text="Delete"
type="btn-danger"
size="btn-xs"
customClass="table--show-on-row-hover"
confirmAction={handleDelete(task, onDelete)}
/>
</td>
</tr>
)
const {arrayOf, func, shape, string} = PropTypes
TasksTable.propTypes = {
tasks: arrayOf(shape()),
onChangeRuleStatus: func,
onDelete: func,
source: shape({
id: string.isRequired,
}).isRequired,
}
TaskRow.propTypes = {
task: shape(),
source: shape(),
onChangeRuleStatus: func,
onDelete: func,
}
export default TasksTable

View File

@ -0,0 +1,113 @@
import React, {PureComponent, SFC} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import {AlertRule, Source} from 'src/types'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
const {colName, colType, colEnabled, colActions} = TASKS_TABLE
interface TasksTableProps {
tasks: AlertRule[]
source: Source
onChangeRuleStatus: (rule: AlertRule) => void
onDelete: (rule: AlertRule) => void
}
interface TaskRowProps {
task: AlertRule
source: Source
onChangeRuleStatus: (rule: AlertRule) => void
onDelete: (rule: AlertRule) => void
}
const TasksTable: SFC<TasksTableProps> = ({
tasks,
source,
onDelete,
onChangeRuleStatus,
}) => (
<table className="table v-center table-highlight">
<thead>
<tr>
<th style={{minWidth: colName}}>Name</th>
<th style={{width: colType}}>Type</th>
<th style={{width: colEnabled}} className="text-center">
Task Enabled
</th>
<th style={{width: colActions}} />
</tr>
</thead>
<tbody>
{_.sortBy(tasks, t => t.id.toLowerCase()).map(task => {
return (
<TaskRow
key={task.id}
task={task}
source={source}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
)
})}
</tbody>
</table>
)
export class TaskRow extends PureComponent<TaskRowProps> {
public handleClickRuleStatusEnabled(task: AlertRule) {
return () => {
this.props.onChangeRuleStatus(task)
}
}
public handleDelete(task: AlertRule) {
return () => {
this.props.onDelete(task)
}
}
public render() {
const {task, source} = this.props
return (
<tr key={task.id}>
<td style={{minWidth: colName}}>
<Link
className="link-success"
to={`/sources/${source.id}/tickscript/${task.id}`}
>
{task.name}
</Link>
</td>
<td style={{width: colType, textTransform: 'capitalize'}}>
{task.type}
</td>
<td style={{width: colEnabled}} className="text-center">
<div className="dark-checkbox">
<input
id={`kapacitor-task-row-task-enabled ${task.id}`}
className="form-control-static"
type="checkbox"
checked={task.status === 'enabled'}
onChange={this.handleClickRuleStatusEnabled(task)}
/>
<label htmlFor={`kapacitor-task-row-task-enabled ${task.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right">
<ConfirmButton
text="Delete"
type="btn-danger"
size="btn-xs"
customClass="table--show-on-row-hover"
confirmAction={this.handleDelete(task)}
/>
</td>
</tr>
)
}
}
export default TasksTable

View File

@ -2,9 +2,14 @@ import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {getActiveKapacitor} from 'shared/apis'
import * as kapacitorActionCreators from '../actions/view'
import KapacitorRules from 'src/kapacitor/components/KapacitorRules'
import SourceIndicator from 'shared/components/SourceIndicator'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import QuestionMarkTooltip from 'shared/components/QuestionMarkTooltip'
class KapacitorRulesPage extends Component {
constructor(props) {
@ -25,12 +30,13 @@ class KapacitorRulesPage extends Component {
this.setState({loading: false, hasKapacitor: !!kapacitor})
}
handleDeleteRule = rule => () => {
handleDeleteRule = rule => {
const {actions} = this.props
actions.deleteRule(rule)
}
handleRuleStatus = rule => () => {
handleRuleStatus = rule => {
const {actions} = this.props
const status = rule.status === 'enabled' ? 'disabled' : 'enabled'
@ -43,19 +49,47 @@ class KapacitorRulesPage extends Component {
const {hasKapacitor, loading} = this.state
return (
<KapacitorRules
source={source}
rules={rules}
hasKapacitor={hasKapacitor}
loading={loading}
onDelete={this.handleDeleteRule}
onChangeRuleStatus={this.handleRuleStatus}
/>
<PageContents source={source}>
<KapacitorRules
source={source}
rules={rules}
hasKapacitor={hasKapacitor}
loading={loading}
onDelete={this.handleDeleteRule}
onChangeRuleStatus={this.handleRuleStatus}
/>
</PageContents>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
const PageContents = ({children}) => (
<div className="page">
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Manage Tasks</h1>
</div>
<div className="page-header__right">
<QuestionMarkTooltip
tipID="manage-tasks--tooltip"
tipContent="<b>Alert Rules</b> generate a TICKscript for<br/>you using our Builder UI.<br/><br/>Not all TICKscripts can be edited<br/>using the Builder."
/>
<SourceIndicator />
</div>
</div>
</div>
<FancyScrollbar className="page-contents fancy-scroll--kapacitor">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">{children}</div>
</div>
</div>
</FancyScrollbar>
</div>
)
const {arrayOf, func, node, shape, string} = PropTypes
KapacitorRulesPage.propTypes = {
source: shape({
@ -80,6 +114,10 @@ KapacitorRulesPage.propTypes = {
}).isRequired,
}
PageContents.propTypes = {
children: node,
}
const mapStateToProps = state => {
return {
rules: Object.values(state.rules),

View File

@ -1,26 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
export const KapacitorTasksPage = React.createClass({
propTypes: {
source: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired, // 'influx-enterprise'
username: PropTypes.string.isRequired,
links: PropTypes.shape({
kapacitors: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
},
getInitialState() {
return {}
},
render() {
return <div className="kapacitorTasks">tasks</div>
},
})
export default KapacitorTasksPage

View File

@ -1,12 +1,6 @@
import KapacitorPage from './containers/KapacitorPage'
import KapacitorRulePage from './containers/KapacitorRulePage'
import KapacitorRulesPage from './containers/KapacitorRulesPage'
import KapacitorTasksPage from './containers/KapacitorTasksPage'
import TickscriptPage from './containers/TickscriptPage'
export {
KapacitorPage,
KapacitorRulePage,
KapacitorRulesPage,
KapacitorTasksPage,
TickscriptPage,
}
export {KapacitorPage, KapacitorRulePage, KapacitorRulesPage, TickscriptPage}

View File

@ -1,5 +1,16 @@
import {AuthLinks, Organization, Role, User} from './auth'
import {Query} from './query'
import {Kapacitor, Source} from './sources'
import {AlertRule, Kapacitor} from './kapacitor'
import {Query, QueryConfig} from './query'
import {Source} from './sources'
export {Query, Source, Kapacitor, AuthLinks, User, Organization, Role}
export {
AuthLinks,
Role,
User,
Organization,
AlertRule,
Kapacitor,
Query,
QueryConfig,
Source,
}

188
ui/src/types/kapacitor.tsx Normal file
View File

@ -0,0 +1,188 @@
import {QueryConfig} from './'
export interface Kapacitor {
id?: string
url: string
name: string
username?: string
password?: string
active: boolean
insecureSkipVerify: boolean
links: {
self: string
}
}
export interface AlertRule {
id?: string
tickscript: TICKScript
query?: QueryConfig
every: string
alertNodes: AlertNodes
message: string
details: string
trigger: string
values: TriggerValues
name: string
type: string
dbrps: DBRP[]
status: string
executing: boolean
error: string
created: string
modified: string
'last-enabled'?: string
}
type TICKScript = string
// AlertNodes defines all possible kapacitor interactions with an alert.
interface AlertNodes {
stateChangesOnly: boolean
useFlapping: boolean
post: Post[]
tcp: TCP[]
email: Email[]
exec: Exec[]
log: Log[]
victorOps: VictorOps[]
pagerDuty: PagerDuty[]
pushover: Pushover[]
sensu: Sensu[]
slack: Slack[]
telegram: Telegram[]
hipChat: HipChat[]
alerta: Alerta[]
opsGenie: OpsGenie[]
talk: Talk[]
}
interface Headers {
[key: string]: string
}
// Post will POST alerts to a destination URL
interface Post {
url: string
headers: Headers
}
// Log sends the output of the alert to a file
interface Log {
filePath: string
}
// Alerta sends the output of the alert to an alerta service
interface Alerta {
token: string
resource: string
event: string
environment: string
group: string
value: string
origin: string
service: string[]
}
// Exec executes a shell command on an alert
interface Exec {
command: string[]
}
// TCP sends the alert to the address
interface TCP {
address: string
}
// Email sends the alert to a list of email addresses
interface Email {
to: string[]
}
// VictorOps sends alerts to the victorops.com service
interface VictorOps {
routingKey: string
}
// PagerDuty sends alerts to the pagerduty.com service
interface PagerDuty {
serviceKey: string
}
// HipChat sends alerts to stride.com
interface HipChat {
room: string
token: string
}
// Sensu sends alerts to sensu or sensuapp.org
interface Sensu {
source: string
handlers: string[]
}
// Pushover sends alerts to pushover.net
interface Pushover {
// UserKey is the User/Group key of your user (or you), viewable when logged
// into the Pushover dashboard. Often referred to as USER_KEY
// in the Pushover documentation.
userKey: string
// Device is the users device name to send message directly to that device,
// rather than all of a user's devices (multiple device names may
// be separated by a comma)
device: string
// Title is your message's title, otherwise your apps name is used
title: string
// URL is a supplementary URL to show with your message
url: string
// URLTitle is a title for your supplementary URL, otherwise just URL is shown
urlTitle: string
// Sound is the name of one of the sounds supported by the device clients to override
// the user's default sound choice
sound: string
}
// Slack sends alerts to a slack.com channel
interface Slack {
channel: string
username: string
iconEmoji: string
}
// Telegram sends alerts to telegram.org
interface Telegram {
chatId: string
parseMode: string
disableWebPagePreview: boolean
disableNotification: boolean
}
// OpsGenie sends alerts to opsgenie.com
interface OpsGenie {
teams: string[]
recipients: string[]
}
// Talk sends alerts to Jane Talk (https://jianliao.com/site)
interface Talk {}
// TriggerValues specifies the alerting logic for a specific trigger type
interface TriggerValues {
change?: string
period?: string
shift?: string
operator?: string
value?: string
rangeValue: string
}
// DBRP represents a database and retention policy for a time series source
interface DBRP {
db: string
rp: string
}

View File

@ -8,7 +8,7 @@ export interface Query {
groupBy: GroupBy
areTagsAccepted: boolean
rawText: string | null
range?: TimeRange | null
range?: DurationRange | null
source?: string
fill: string
status?: Status
@ -53,7 +53,28 @@ export interface Status {
success?: string
}
export interface TimeRange {
export interface DurationRange {
lower: string
upper?: string
}
interface TimeShift {
label: string
unit: string
quantity: string
}
export interface QueryConfig {
id?: string
database: string
measurement: string
retentionPolicy: string
fields: Field[]
tags: Tags
groupBy: GroupBy
areTagsAccepted: boolean
fill?: string
rawText: string
range: DurationRange
shifts: TimeShift[]
}

View File

@ -1,3 +1,5 @@
import {Kapacitor} from './'
export interface Source {
id: string
name: string
@ -13,7 +15,7 @@ export interface Source {
metaUrl?: string
}
export interface SourceLinks {
interface SourceLinks {
self: string
kapacitors: string
proxy: string
@ -24,16 +26,3 @@ export interface SourceLinks {
databases: string
roles?: string
}
export interface Kapacitor {
id?: string
url: string
name: string
username?: string
password?: string
insecureSkipVerify: boolean
active: boolean
links: {
self: string
}
}

View File

@ -0,0 +1,139 @@
import React from 'react'
import {shallow} from 'enzyme'
import _ from 'lodash'
import KapacitorRules from 'src/kapacitor/components/KapacitorRules'
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
import TasksTable from 'src/kapacitor/components/TasksTable'
import {source, kapacitorRules} from 'test/resources'
describe('Kapacitor.Containers.KapacitorRules', () => {
const props = {
source,
rules: kapacitorRules,
hasKapacitor: true,
loading: false,
onDelete: () => {},
onChangeRuleStatus: () => {},
}
describe('rendering', () => {
it('renders KapacitorRules', () => {
const wrapper = shallow(<KapacitorRules {...props} />)
expect(wrapper.exists()).toBe(true)
})
it('renders KapacitorRulesTable', () => {
const wrapper = shallow(<KapacitorRules {...props} />)
const kapacitorRulesTable = wrapper.find(KapacitorRulesTable)
expect(kapacitorRulesTable.length).toEqual(1)
const tasksTable = wrapper.find(TasksTable)
expect(tasksTable.length).toEqual(1)
})
it('renders TasksTable', () => {
const wrapper = shallow(<KapacitorRules {...props} />)
const tasksTable = wrapper.find(TasksTable)
expect(tasksTable.length).toEqual(1)
})
describe('rows in KapacitorRulesTable and TasksTable', () => {
const findRows = (root, reactTable) =>
root
.find(reactTable)
.dive()
.find('tbody')
.children()
.map(child => {
const ruleID = child.key()
const elRow = child.dive()
const elLabel = elRow.find('label')
const {htmlFor} = elLabel.props()
const elCheckbox = elRow.find({type: 'checkbox'})
const {checked, id} = elCheckbox.props()
return {
row: {
el: elRow,
label: {
el: elLabel,
htmlFor,
},
checkbox: {
el: elCheckbox,
checked,
id,
},
},
rule: {
id: ruleID, // rule.id
},
}
})
const containsAnyDuplicate = arr => _.uniq(arr).length !== arr.length
let wrapper
let rulesRows
let tasksRows
beforeEach(() => {
wrapper = shallow(<KapacitorRules {...props} />)
rulesRows = findRows(wrapper, KapacitorRulesTable)
tasksRows = findRows(wrapper, TasksTable)
})
it('renders every rule/task checkbox with unique html id', () => {
const allCheckboxIDs = rulesRows
.map(r => r.row.checkbox.id)
.concat(tasksRows.map(r => r.row.checkbox.id))
expect(containsAnyDuplicate(allCheckboxIDs)).toEqual(false)
})
it('renders each rule/task table row label with unique "for" attribute', () => {
const allCheckboxLabelFors = rulesRows
.map(r => r.row.label.htmlFor)
.concat(tasksRows.map(r => r.row.label.htmlFor))
expect(containsAnyDuplicate(allCheckboxLabelFors)).toEqual(false)
})
it('renders one corresponding task row for each rule row', () => {
expect(rulesRows.length).toBeLessThanOrEqual(tasksRows.length)
rulesRows.forEach(ruleRow => {
expect(
tasksRows.filter(taskRow => taskRow.rule.id === ruleRow.rule.id)
.length
).toEqual(1)
})
})
it('renders corresponding rule/task rows with the same enabled status', () => {
const correspondingRows = []
rulesRows.forEach(ruleRow => {
const taskRow = tasksRows
.filter(t => t.rule.id === ruleRow.rule.id)
.pop()
if (taskRow) {
correspondingRows.push({ruleRow, taskRow})
}
})
correspondingRows.forEach(({ruleRow, taskRow}) => {
expect(ruleRow.row.checkbox.checked).toEqual(
taskRow.row.checkbox.checked
)
})
})
})
})
})

View File

@ -0,0 +1,57 @@
import React from 'react'
import {shallow} from 'enzyme'
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
import {RuleRow} from 'src/kapacitor/components/KapacitorRulesTable'
import {source, kapacitorRules} from 'test/resources'
describe('Kapacitor.Components.KapacitorRulesTable', () => {
describe('rendering', () => {
const props = {
source,
rules: kapacitorRules,
onDelete: () => {},
onChangeRuleStatus: () => {},
}
it('renders KapacitorRulesTable', () => {
const wrapper = shallow(<KapacitorRulesTable {...props} />)
expect(wrapper.exists()).toBe(true)
})
})
})
describe('Kapacitor.Containers.KapacitorRulesTable.RuleRow', () => {
const props = {
source,
rule: kapacitorRules[0],
onDelete: () => {},
onChangeRuleStatus: jest.fn(),
}
afterEach(() => {
jest.clearAllMocks()
})
describe('rendering', () => {
it('renders RuleRow', () => {
const wrapper = shallow(<RuleRow {...props} />)
expect(wrapper.exists()).toBe(true)
})
})
describe('user interaction', () => {
it('calls onChangeRuleStatus when checkbox is effectively clicked', () => {
const wrapper = shallow(<RuleRow {...props} />)
const checkbox = wrapper.find({type: 'checkbox'})
checkbox.simulate('change')
expect(props.onChangeRuleStatus).toHaveBeenCalledTimes(1)
expect(props.onChangeRuleStatus).toHaveBeenCalledWith(kapacitorRules[0])
})
})
})

View File

@ -0,0 +1,43 @@
import React from 'react'
import {shallow} from 'enzyme'
import TasksTable from 'src/kapacitor/components/TasksTable'
import {TaskRow} from 'src/kapacitor/components/TasksTable'
import {source, kapacitorRules} from 'test/resources'
describe('Kapacitor.Components.TasksTable', () => {
describe('rendering', () => {
const props = {
source,
tasks: kapacitorRules,
onDelete: () => {},
onChangeRuleStatus: () => {},
}
it('renders TasksTable', () => {
const wrapper = shallow(<TasksTable {...props} />)
expect(wrapper.exists()).toBe(true)
})
})
describe('user interaction', () => {
const props = {
source,
task: kapacitorRules[3],
onDelete: () => {},
onChangeRuleStatus: jest.fn(),
}
it('calls onChangeRuleStatus when checkbox is effectively clicked', () => {
const wrapper = shallow(<TaskRow {...props} />)
const checkbox = wrapper.find({type: 'checkbox'})
checkbox.simulate('change')
expect(props.onChangeRuleStatus).toHaveBeenCalledTimes(1)
expect(props.onChangeRuleStatus).toHaveBeenCalledWith(kapacitorRules[3])
})
})
})

View File

@ -63,6 +63,340 @@ export const kapacitor = {
},
}
export const kapacitorRules = [
{
id: 'chronograf-v1-1bb60c5d-9c46-4601-8fdd-930ac5d2ae3d',
tickscript:
"var db = 'telegraf'\n\nvar rp = 'autogen'\n\nvar measurement = 'cpu'\n\nvar groupBy = ['cpu']\n\nvar whereFilter = lambda: (\"cpu\" != 'cpu-total' OR \"cpu\" != 'cpu1')\n\nvar period = 1h\n\nvar name = 'asdfasdfasdfasdfbob'\n\nvar idVar = name + ':{{.Group}}'\n\nvar message = ''\n\nvar idTag = 'alertID'\n\nvar levelTag = 'level'\n\nvar messageField = 'message'\n\nvar durationField = 'duration'\n\nvar outputDB = 'chronograf'\n\nvar outputRP = 'autogen'\n\nvar outputMeasurement = 'alerts'\n\nvar triggerType = 'deadman'\n\nvar threshold = 0.0\n\nvar data = stream\n |from()\n .database(db)\n .retentionPolicy(rp)\n .measurement(measurement)\n .groupBy(groupBy)\n .where(whereFilter)\n\nvar trigger = data\n |deadman(threshold, period)\n .stateChangesOnly()\n .message(message)\n .id(idVar)\n .idTag(idTag)\n .levelTag(levelTag)\n .messageField(messageField)\n .durationField(durationField)\n .stateChangesOnly()\n .pushover()\n .pushover()\n .sensu()\n .source('Kapacitorsdfasdf')\n .handlers()\n\ntrigger\n |eval(lambda: \"emitted\")\n .as('value')\n .keep('value', messageField, durationField)\n |eval(lambda: float(\"value\"))\n .as('value')\n .keep()\n |influxDBOut()\n .create()\n .database(outputDB)\n .retentionPolicy(outputRP)\n .measurement(outputMeasurement)\n .tag('alertName', name)\n .tag('triggerType', triggerType)\n\ntrigger\n |httpOut('output')\n",
query: {
id: 'chronograf-v1-1bb60c5d-9c46-4601-8fdd-930ac5d2ae3d',
database: 'telegraf',
measurement: 'cpu',
retentionPolicy: 'autogen',
fields: [],
tags: {
cpu: ['cpu-total', 'cpu1'],
},
groupBy: {
time: '',
tags: ['cpu'],
},
areTagsAccepted: false,
rawText: null,
range: null,
shifts: null,
},
every: '',
alertNodes: {
typeOf: 'alert',
stateChangesOnly: true,
useFlapping: false,
post: [],
tcp: [],
email: [],
exec: [],
log: [],
victorOps: [],
pagerDuty: [],
pushover: [
{
userKey: '',
device: '',
title: '',
url: '',
urlTitle: '',
sound: '',
},
{
userKey: '',
device: '',
title: '',
url: '',
urlTitle: '',
sound: '',
},
],
sensu: [
{
source: 'Kapacitorsdfasdf',
handlers: [],
},
],
slack: [],
telegram: [],
hipChat: [],
alerta: [],
opsGenie: [],
talk: [],
},
message: '',
details: '',
trigger: 'deadman',
values: {
period: '1h0m0s',
rangeValue: '',
},
name: 'asdfasdfasdfasdfbob',
type: 'stream',
dbrps: [
{
db: 'telegraf',
rp: 'autogen',
},
],
status: 'enabled',
executing: true,
error: '',
created: '2018-01-05T15:40:48.195743458-08:00',
modified: '2018-03-13T17:17:23.991640555-07:00',
'last-enabled': '2018-03-13T17:17:23.991640555-07:00',
links: {
self:
'/chronograf/v1/sources/1/kapacitors/1/rules/chronograf-v1-1bb60c5d-9c46-4601-8fdd-930ac5d2ae3d',
kapacitor:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-1bb60c5d-9c46-4601-8fdd-930ac5d2ae3d',
output:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-1bb60c5d-9c46-4601-8fdd-930ac5d2ae3d%2Foutput',
},
},
{
id: 'chronograf-v1-75b638b0-1530-4163-adab-c9631386e0a2',
tickscript:
"var db = 'telegraf'\n\nvar rp = 'autogen'\n\nvar measurement = 'disk'\n\nvar groupBy = []\n\nvar whereFilter = lambda: TRUE\n\nvar name = 'Untitled bob'\n\nvar idVar = name + ':{{.Group}}'\n\nvar message = ''\n\nvar idTag = 'alertID'\n\nvar levelTag = 'level'\n\nvar messageField = 'message'\n\nvar durationField = 'duration'\n\nvar outputDB = 'chronograf'\n\nvar outputRP = 'autogen'\n\nvar outputMeasurement = 'alerts'\n\nvar triggerType = 'threshold'\n\nvar crit = 0\n\nvar data = stream\n |from()\n .database(db)\n .retentionPolicy(rp)\n .measurement(measurement)\n .groupBy(groupBy)\n .where(whereFilter)\n |eval(lambda: \"inodes_free\")\n .as('value')\n\nvar trigger = data\n |alert()\n .crit(lambda: \"value\" == crit)\n .stateChangesOnly()\n .message(message)\n .id(idVar)\n .idTag(idTag)\n .levelTag(levelTag)\n .messageField(messageField)\n .durationField(durationField)\n .stateChangesOnly()\n .email()\n .pagerDuty()\n .alerta()\n .environment('bob')\n .origin('kapacitoadfr')\n .services()\n\ntrigger\n |eval(lambda: float(\"value\"))\n .as('value')\n .keep()\n |influxDBOut()\n .create()\n .database(outputDB)\n .retentionPolicy(outputRP)\n .measurement(outputMeasurement)\n .tag('alertName', name)\n .tag('triggerType', triggerType)\n\ntrigger\n |httpOut('output')\n",
query: {
id: 'chronograf-v1-75b638b0-1530-4163-adab-c9631386e0a2',
database: 'telegraf',
measurement: 'disk',
retentionPolicy: 'autogen',
fields: [
{
value: 'inodes_free',
type: 'field',
alias: '',
},
],
tags: {},
groupBy: {
time: '',
tags: [],
},
areTagsAccepted: false,
rawText: null,
range: null,
shifts: null,
},
every: '',
alertNodes: {
typeOf: 'alert',
stateChangesOnly: true,
useFlapping: false,
post: [],
tcp: [],
email: [
{
to: [],
},
],
exec: [],
log: [],
victorOps: [],
pagerDuty: [
{
serviceKey: '',
},
],
pushover: [],
sensu: [],
slack: [],
telegram: [],
hipChat: [],
alerta: [
{
token: '',
resource: '',
event: '',
environment: 'bob',
group: '',
value: '',
origin: 'kapacitoadfr',
service: [],
},
],
opsGenie: [],
talk: [],
},
message: '',
details: '',
trigger: 'threshold',
values: {
operator: 'equal to',
value: '0',
rangeValue: '',
},
name: 'Untitled bob',
type: 'stream',
dbrps: [
{
db: 'telegraf',
rp: 'autogen',
},
],
status: 'disabled',
executing: false,
error: '',
created: '2018-01-05T15:41:22.759905067-08:00',
modified: '2018-03-14T18:46:37.940091231-07:00',
'last-enabled': '2018-03-14T18:46:32.409262103-07:00',
links: {
self:
'/chronograf/v1/sources/1/kapacitors/1/rules/chronograf-v1-75b638b0-1530-4163-adab-c9631386e0a2',
kapacitor:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-75b638b0-1530-4163-adab-c9631386e0a2',
output:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-75b638b0-1530-4163-adab-c9631386e0a2%2Foutput',
},
},
{
id: 'chronograf-v1-7734918d-b8b6-460d-a416-34767ba76faa',
tickscript:
"var db = 'telegraf'\n\nvar rp = 'autogen'\n\nvar measurement = 'cpu'\n\nvar groupBy = []\n\nvar whereFilter = lambda: (\"host\" == 'Bobs-MacBook-Pro.local')\n\nvar period = 24h\n\nvar name = 'xena'\n\nvar idVar = name + ':{{.Group}}'\n\nvar message = ''\n\nvar idTag = 'alertID'\n\nvar levelTag = 'level'\n\nvar messageField = 'message'\n\nvar durationField = 'duration'\n\nvar outputDB = 'chronograf'\n\nvar outputRP = 'autogen'\n\nvar outputMeasurement = 'alerts'\n\nvar triggerType = 'deadman'\n\nvar threshold = 0.0\n\nvar data = stream\n |from()\n .database(db)\n .retentionPolicy(rp)\n .measurement(measurement)\n .groupBy(groupBy)\n .where(whereFilter)\n\nvar trigger = data\n |deadman(threshold, period)\n .stateChangesOnly()\n .message(message)\n .id(idVar)\n .idTag(idTag)\n .levelTag(levelTag)\n .messageField(messageField)\n .durationField(durationField)\n .hipChat()\n .room('asdf')\n\ntrigger\n |eval(lambda: \"emitted\")\n .as('value')\n .keep('value', messageField, durationField)\n |eval(lambda: float(\"value\"))\n .as('value')\n .keep()\n |influxDBOut()\n .create()\n .database(outputDB)\n .retentionPolicy(outputRP)\n .measurement(outputMeasurement)\n .tag('alertName', name)\n .tag('triggerType', triggerType)\n\ntrigger\n |httpOut('output')\n",
query: {
id: 'chronograf-v1-7734918d-b8b6-460d-a416-34767ba76faa',
database: 'telegraf',
measurement: 'cpu',
retentionPolicy: 'autogen',
fields: [],
tags: {
host: ['Bobs-MacBook-Pro.local'],
},
groupBy: {
time: '',
tags: [],
},
areTagsAccepted: true,
rawText: null,
range: null,
shifts: null,
},
every: '',
alertNodes: {
typeOf: 'alert',
stateChangesOnly: true,
useFlapping: false,
post: [],
tcp: [],
email: [],
exec: [],
log: [],
victorOps: [],
pagerDuty: [],
pushover: [],
sensu: [],
slack: [],
telegram: [],
hipChat: [
{
room: 'asdf',
token: '',
},
],
alerta: [],
opsGenie: [],
talk: [],
},
message: '',
details: '',
trigger: 'deadman',
values: {
period: '24h0m0s',
rangeValue: '',
},
name: 'xena',
type: 'stream',
dbrps: [
{
db: 'telegraf',
rp: 'autogen',
},
],
status: 'disabled',
executing: false,
error: '',
created: '2018-01-05T15:44:54.657212781-08:00',
modified: '2018-03-13T17:17:19.099800735-07:00',
'last-enabled': '2018-03-13T17:17:15.964357573-07:00',
links: {
self:
'/chronograf/v1/sources/1/kapacitors/1/rules/chronograf-v1-7734918d-b8b6-460d-a416-34767ba76faa',
kapacitor:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-7734918d-b8b6-460d-a416-34767ba76faa',
output:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-7734918d-b8b6-460d-a416-34767ba76faa%2Foutput',
},
},
{
// if rule has no `query` key, it will display as a tickscript task only
id: 'chronograf-v1-7734918d-b8b6-460d-a416-34767ba76aac',
tickscript:
"var db = 'telegraf'\n\nvar rp = 'autogen'\n\nvar measurement = 'cpu'\n\nvar groupBy = []\n\nvar whereFilter = lambda: (\"host\" == 'Bobs-MacBook-Pro.local')\n\nvar period = 24h\n\nvar name = 'xena'\n\nvar idVar = name + ':{{.Group}}'\n\nvar message = ''\n\nvar idTag = 'alertID'\n\nvar levelTag = 'level'\n\nvar messageField = 'message'\n\nvar durationField = 'duration'\n\nvar outputDB = 'chronograf'\n\nvar outputRP = 'autogen'\n\nvar outputMeasurement = 'alerts'\n\nvar triggerType = 'deadman'\n\nvar threshold = 0.0\n\nvar data = stream\n |from()\n .database(db)\n .retentionPolicy(rp)\n .measurement(measurement)\n .groupBy(groupBy)\n .where(whereFilter)\n\nvar trigger = data\n |deadman(threshold, period)\n .stateChangesOnly()\n .message(message)\n .id(idVar)\n .idTag(idTag)\n .levelTag(levelTag)\n .messageField(messageField)\n .durationField(durationField)\n .hipChat()\n .room('asdf')\n\ntrigger\n |eval(lambda: \"emitted\")\n .as('value')\n .keep('value', messageField, durationField)\n |eval(lambda: float(\"value\"))\n .as('value')\n .keep()\n |influxDBOut()\n .create()\n .database(outputDB)\n .retentionPolicy(outputRP)\n .measurement(outputMeasurement)\n .tag('alertName', name)\n .tag('triggerType', triggerType)\n\ntrigger\n |httpOut('output')\n",
every: '',
alertNodes: {
typeOf: 'alert',
stateChangesOnly: true,
useFlapping: false,
post: [],
tcp: [],
email: [],
exec: [],
log: [],
victorOps: [],
pagerDuty: [],
pushover: [],
sensu: [],
slack: [],
telegram: [],
hipChat: [
{
room: 'asdf',
token: '',
},
],
alerta: [],
opsGenie: [],
talk: [],
},
message: '',
details: '',
trigger: 'deadman',
values: {
period: '24h0m0s',
rangeValue: '',
},
name: 'pineapples',
type: 'stream',
dbrps: [
{
db: 'telegraf',
rp: 'autogen',
},
],
status: 'enabled',
executing: false,
error: '',
created: '2018-01-05T15:44:54.657212781-08:00',
modified: '2018-03-13T17:17:19.099800735-07:00',
'last-enabled': '2018-03-13T17:17:15.964357573-07:00',
links: {
self:
'/chronograf/v1/sources/1/kapacitors/1/rules/chronograf-v1-7734918d-b8b6-460d-a416-34767ba76aac',
kapacitor:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-7734918d-b8b6-460d-a416-34767ba76aac',
output:
'/chronograf/v1/sources/1/kapacitors/1/proxy?path=%2Fkapacitor%2Fv1%2Ftasks%2Fchronograf-v1-7734918d-b8b6-460d-a416-34767ba76aac%2Foutput',
},
},
]
export const authLinks = {
allUsers: '/chronograf/v1/users',
auth: [