diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d2e1e50fe..cc98df4b78 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
### Features
1. [#1885](https://github.com/influxdata/chronograf/pull/1885): Add `fill` options to data explorer and dashboard queries
1. [#1978](https://github.com/influxdata/chronograf/pull/1978): Support editing kapacitor TICKScript
+1. [#1721](https://github.com/influxdata/chronograf/pull/1721): Introduce the TICKscript editor UI
1. [#1992](https://github.com/influxdata/chronograf/pull/1992): Add .csv download button to data explorer
### UI Improvements
@@ -146,6 +147,8 @@
1. [#1738](https://github.com/influxdata/chronograf/pull/1738): Add shared secret JWT authorization to InfluxDB
1. [#1724](https://github.com/influxdata/chronograf/pull/1724): Add Pushover alert support
1. [#1762](https://github.com/influxdata/chronograf/pull/1762): Restore all supported Kapacitor services when creating rules, and add most optional message parameters
+1. [#1681](https://github.com/influxdata/chronograf/pull/1681): Add the ability to select Custom Time Ranges in the Hostpages, Data Explorer, and Dashboards.
+1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts
### UI Improvements
1. [#1707](https://github.com/influxdata/chronograf/pull/1707): Polish alerts table in status page to wrap text less
diff --git a/server/kapacitors.go b/server/kapacitors.go
index a0a838cb31..e7982a8cb3 100644
--- a/server/kapacitors.go
+++ b/server/kapacitors.go
@@ -39,6 +39,8 @@ type kapaLinks struct {
Proxy string `json:"proxy"` // URL location of proxy endpoint for this source
Self string `json:"self"` // Self link mapping to this resource
Rules string `json:"rules"` // Rules link for defining roles alerts for kapacitor
+ Tasks string `json:"tasks"` // Tasks link to define a task against the proxy
+ Ping string `json:"ping"` // Ping path to kapacitor
}
type kapacitor struct {
@@ -108,6 +110,8 @@ func newKapacitor(srv chronograf.Server) kapacitor {
Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID),
Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID),
Rules: fmt.Sprintf("%s/%d/kapacitors/%d/rules", httpAPISrcs, srv.SrcID, srv.ID),
+ Tasks: fmt.Sprintf("%s/%d/kapacitors/%d/proxy?path=/kapacitor/v1/tasks", httpAPISrcs, srv.SrcID, srv.ID),
+ Ping: fmt.Sprintf("%s/%d/kapacitors/%d/proxy?path=/kapacitor/v1/ping", httpAPISrcs, srv.SrcID, srv.ID),
},
}
}
diff --git a/ui/package.json b/ui/package.json
index ce6f46bf6d..4d6a74b71f 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -95,6 +95,7 @@
"webpack-dev-server": "^1.14.1"
},
"dependencies": {
+ "@skidding/react-codemirror": "^1.0.1",
"axios": "^0.13.1",
"bignumber.js": "^4.0.2",
"bootstrap": "^3.3.7",
@@ -112,6 +113,7 @@
"query-string": "^5.0.0",
"react": "^15.0.2",
"react-addons-shallow-compare": "^15.0.2",
+ "react-codemirror": "^1.0.0",
"react-custom-scrollbars": "^4.1.1",
"react-dimensions": "^1.2.0",
"react-dom": "^15.0.2",
diff --git a/ui/src/admin/components/RoleRow.js b/ui/src/admin/components/RoleRow.js
index 2a86465582..d923ae3330 100644
--- a/ui/src/admin/components/RoleRow.js
+++ b/ui/src/admin/components/RoleRow.js
@@ -10,7 +10,7 @@ import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
import {ROLES_TABLE} from 'src/admin/constants/tableSizing'
const RoleRow = ({
- role: {name, permissions, users},
+ role: {name: roleName, permissions, users = []},
role,
allUsers,
allPermissions,
@@ -23,12 +23,14 @@ const RoleRow = ({
onUpdateRoleUsers,
onUpdateRolePermissions,
}) => {
- const handleUpdateUsers = u => {
- onUpdateRoleUsers(role, u.map(n => ({name: n})))
+ const handleUpdateUsers = usrs => {
+ onUpdateRoleUsers(role, usrs)
}
const handleUpdatePermissions = allowed => {
- onUpdateRolePermissions(role, [{scope: 'all', allowed}])
+ onUpdateRolePermissions(role, [
+ {scope: 'all', allowed: allowed.map(({name}) => name)},
+ ])
}
const perms = _.get(permissions, ['0', 'allowed'], [])
@@ -62,13 +64,13 @@ const RoleRow = ({
return (
- {name}
+ {roleName}
|
{allPermissions && allPermissions.length
? ({name}))}
+ selectedItems={perms.map(name => ({name}))}
label={perms.length ? '' : 'Select Permissions'}
onApply={handleUpdatePermissions}
buttonSize="btn-xs"
@@ -85,9 +87,9 @@ const RoleRow = ({
{allUsers && allUsers.length
? u.name)}
- selectedItems={users === undefined ? [] : users.map(u => u.name)}
- label={users && users.length ? '' : 'Select Users'}
+ items={allUsers}
+ selectedItems={users}
+ label={users.length ? '' : 'Select Users'}
onApply={handleUpdateUsers}
buttonSize="btn-xs"
buttonColor="btn-primary"
diff --git a/ui/src/admin/components/UserRow.js b/ui/src/admin/components/UserRow.js
index 9d5589501c..8d49486621 100644
--- a/ui/src/admin/components/UserRow.js
+++ b/ui/src/admin/components/UserRow.js
@@ -12,7 +12,7 @@ import ChangePassRow from 'src/admin/components/ChangePassRow'
import {USERS_TABLE} from 'src/admin/constants/tableSizing'
const UserRow = ({
- user: {name, roles, permissions, password},
+ user: {name, roles = [], permissions, password},
user,
allRoles,
allPermissions,
@@ -27,14 +27,15 @@ const UserRow = ({
onUpdateRoles,
onUpdatePassword,
}) => {
- const handleUpdatePermissions = allowed => {
+ const handleUpdatePermissions = perms => {
+ const allowed = perms.map(p => p.name)
onUpdatePermissions(user, [{scope: 'all', allowed}])
}
const handleUpdateRoles = roleNames => {
onUpdateRoles(
user,
- allRoles.filter(r => roleNames.find(rn => rn === r.name))
+ allRoles.filter(r => roleNames.find(rn => rn.name === r.name))
)
}
@@ -42,6 +43,8 @@ const UserRow = ({
onUpdatePassword(user, password)
}
+ const perms = _.get(permissions, ['0', 'allowed'], [])
+
if (isEditing) {
return (
@@ -85,13 +88,9 @@ const UserRow = ({
{hasRoles
?
r.name)}
- selectedItems={
- roles
- ? roles.map(r => r.name)
- : [] /* TODO remove check when server returns empty list */
- }
- label={roles && roles.length ? '' : 'Select Roles'}
+ items={allRoles}
+ selectedItems={roles.map(r => ({name: r.name}))}
+ label={roles.length ? '' : 'Select Roles'}
onApply={handleUpdateRoles}
buttonSize="btn-xs"
buttonColor="btn-primary"
@@ -104,8 +103,8 @@ const UserRow = ({
{allPermissions && allPermissions.length
? ({name: p}))}
+ selectedItems={perms.map(p => ({name: p}))}
label={
permissions && permissions.length ? '' : 'Select Permissions'
}
diff --git a/ui/src/external/codemirror.js b/ui/src/external/codemirror.js
new file mode 100644
index 0000000000..3129a886d6
--- /dev/null
+++ b/ui/src/external/codemirror.js
@@ -0,0 +1,349 @@
+/* eslint-disable */
+const CodeMirror = require('codemirror')
+
+CodeMirror.defineSimpleMode = function(name, states) {
+ CodeMirror.defineMode(name, function(config) {
+ return CodeMirror.simpleMode(config, states)
+ })
+}
+
+CodeMirror.simpleMode = function(config, states) {
+ ensureState(states, 'start')
+ const states_ = {},
+ meta = states.meta || {}
+ let hasIndentation = false
+ for (const state in states) {
+ if (state !== meta && states.hasOwnProperty(state)) {
+ const list = (states_[state] = []),
+ orig = states[state]
+ for (let i = 0; i < orig.length; i++) {
+ const data = orig[i]
+ list.push(new Rule(data, states))
+ if (data.indent || data.dedent) {
+ hasIndentation = true
+ }
+ }
+ }
+ }
+ const mode = {
+ startState() {
+ return {
+ state: 'start',
+ pending: null,
+ local: null,
+ localState: null,
+ indent: hasIndentation ? [] : null,
+ }
+ },
+ copyState(state) {
+ const s = {
+ state: state.state,
+ pending: state.pending,
+ local: state.local,
+ localState: null,
+ indent: state.indent && state.indent.slice(0),
+ }
+ if (state.localState) {
+ s.localState = CodeMirror.copyState(state.local.mode, state.localState)
+ }
+ if (state.stack) {
+ s.stack = state.stack.slice(0)
+ }
+ for (let pers = state.persistentStates; pers; pers = pers.next) {
+ s.persistentStates = {
+ mode: pers.mode,
+ spec: pers.spec,
+ state:
+ pers.state === state.localState
+ ? s.localState
+ : CodeMirror.copyState(pers.mode, pers.state),
+ next: s.persistentStates,
+ }
+ }
+ return s
+ },
+ token: tokenFunction(states_, config),
+ innerMode(state) {
+ return state.local && {mode: state.local.mode, state: state.localState}
+ },
+ indent: indentFunction(states_, meta),
+ }
+ if (meta) {
+ for (const prop in meta) {
+ if (meta.hasOwnProperty(prop)) {
+ mode[prop] = meta[prop]
+ }
+ }
+ }
+ return mode
+}
+
+function ensureState(states, name) {
+ if (!states.hasOwnProperty(name)) {
+ throw new Error(`Undefined state ${name} in simple mode`)
+ }
+}
+
+function toRegex(val, caret) {
+ if (!val) {
+ return /(?:)/
+ }
+ let flags = ''
+ if (val instanceof RegExp) {
+ if (val.ignoreCase) {
+ flags = 'i'
+ }
+ val = val.source
+ } else {
+ val = String(val)
+ }
+ return new RegExp(`${caret === false ? '' : '^'}(?:${val})`, flags)
+}
+
+function asToken(val) {
+ if (!val) {
+ return null
+ }
+ if (val.apply) {
+ return val
+ }
+ if (typeof val === 'string') {
+ return val.replace(/\./g, ' ')
+ }
+ const result = []
+ for (let i = 0; i < val.length; i++) {
+ result.push(val[i] && val[i].replace(/\./g, ' '))
+ }
+ return result
+}
+
+function Rule(data, states) {
+ if (data.next || data.push) {
+ ensureState(states, data.next || data.push)
+ }
+ this.regex = toRegex(data.regex)
+ this.token = asToken(data.token)
+ this.data = data
+}
+
+function tokenFunction(states, config) {
+ return function(stream, state) {
+ if (state.pending) {
+ const pend = state.pending.shift()
+ if (state.pending.length === 0) {
+ state.pending = null
+ }
+ stream.pos += pend.text.length
+ return pend.token
+ }
+
+ if (state.local) {
+ let tok, m
+ if (state.local.end && stream.match(state.local.end)) {
+ tok = state.local.endToken || null
+ state.local = state.localState = null
+ return tok
+ }
+
+ tok = state.local.mode.token(stream, state.localState)
+ if (
+ state.local.endScan &&
+ (m = state.local.endScan.exec(stream.current()))
+ ) {
+ stream.pos = stream.start + m.index
+ }
+ return tok
+ }
+
+ const curState = states[state.state]
+ for (let i = 0; i < curState.length; i++) {
+ const rule = curState[i]
+ const matches =
+ (!rule.data.sol || stream.sol()) && stream.match(rule.regex)
+ if (matches) {
+ if (rule.data.next) {
+ state.state = rule.data.next
+ } else if (rule.data.push) {
+ ;(state.stack || (state.stack = [])).push(state.state)
+ state.state = rule.data.push
+ } else if (rule.data.pop && state.stack && state.stack.length) {
+ state.state = state.stack.pop()
+ }
+
+ if (rule.data.mode) {
+ enterLocalMode(config, state, rule.data.mode, rule.token)
+ }
+ if (rule.data.indent) {
+ state.indent.push(stream.indentation() + config.indentUnit)
+ }
+ if (rule.data.dedent) {
+ state.indent.pop()
+ }
+ let token = rule.token
+ if (token && token.apply) {
+ token = token(matches)
+ }
+ if (matches.length > 2) {
+ state.pending = []
+ for (let j = 2; j < matches.length; j++) {
+ if (matches[j]) {
+ state.pending.push({text: matches[j], token: rule.token[j - 1]})
+ }
+ }
+ stream.backUp(
+ matches[0].length - (matches[1] ? matches[1].length : 0)
+ )
+ return token[0]
+ } else if (token && token.join) {
+ return token[0]
+ }
+ return token
+ }
+ }
+ stream.next()
+ return null
+ }
+}
+
+function cmp(a, b) {
+ if (a === b) {
+ return true
+ }
+ if (!a || typeof a !== 'object' || !b || typeof b !== 'object') {
+ return false
+ }
+ let props = 0
+ for (const prop in a) {
+ if (a.hasOwnProperty(prop)) {
+ if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) {
+ return false
+ }
+ props += 1
+ }
+ }
+ for (const prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ props -= 1
+ }
+ }
+ return props === 0
+}
+
+function enterLocalMode(config, state, spec, token) {
+ let pers
+ if (spec.persistent) {
+ for (let p = state.persistentStates; p && !pers; p = p.next) {
+ if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode === p.mode) {
+ pers = p
+ }
+ }
+ }
+ const mode = pers
+ ? pers.mode
+ : spec.mode || CodeMirror.getMode(config, spec.spec)
+ const lState = pers ? pers.state : CodeMirror.startState(mode)
+ if (spec.persistent && !pers) {
+ state.persistentStates = {
+ mode,
+ spec: spec.spec,
+ state: lState,
+ next: state.persistentStates,
+ }
+ }
+
+ state.localState = lState
+ state.local = {
+ mode,
+ end: spec.end && toRegex(spec.end),
+ endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false),
+ endToken: token && token.join ? token[token.length - 1] : token,
+ }
+}
+
+function indexOf(val, arr) {
+ for (let i = 0; i < arr.length; i++) {
+ if (arr[i] === val) {
+ return true
+ }
+ }
+}
+
+function indentFunction(states, meta) {
+ return function(state, textAfter, line) {
+ if (state.local && state.local.mode.indent) {
+ return state.local.mode.indent(state.localState, textAfter, line)
+ }
+ if (
+ state.indent === null ||
+ state.local ||
+ (meta.dontIndentStates &&
+ indexOf(state.state, meta.dontIndentStates) > -1)
+ ) {
+ return CodeMirror.Pass
+ }
+
+ let pos = state.indent.length - 1,
+ rules = states[state.state]
+ scan: for (;;) {
+ for (let i = 0; i < rules.length; i++) {
+ const rule = rules[i]
+ if (rule.data.dedent && rule.data.dedentIfLineStart !== false) {
+ const m = rule.regex.exec(textAfter)
+ if (m && m[0]) {
+ pos -= 1
+ if (rule.next || rule.push) {
+ rules = states[rule.next || rule.push]
+ }
+ textAfter = textAfter.slice(m[0].length)
+ continue scan
+ }
+ }
+ }
+ break
+ }
+ return pos < 0 ? 0 : state.indent[pos]
+ }
+}
+
+CodeMirror.defineSimpleMode('tickscript', {
+ // The start state contains the rules that are intially used
+ start: [
+ // The regex matches the token, the token property contains the type
+ {regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: 'string.double'},
+ {regex: /'(?:[^\\]|\\.)*?(?:'|$)/, token: 'string.single'},
+ {
+ regex: /(function)(\s+)([a-z$][\w$]*)/,
+ token: ['keyword', null, 'variable-2'],
+ },
+ // Rules are matched in the order in which they appear, so there is
+ // no ambiguity between this one and the one above
+ {
+ regex: /(?:var|return|if|for|while|else|do|this|stream|batch|influxql|lambda)/,
+ token: 'keyword',
+ },
+ {regex: /true|false|null|undefined|TRUE|FALSE/, token: 'atom'},
+ {
+ regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,
+ token: 'number',
+ },
+ {regex: /\/\/.*/, token: 'comment'},
+ {regex: /\/(?:[^\\]|\\.)*?\//, token: 'variable-3'},
+ // A next property will cause the mode to move to a different state
+ {regex: /\/\*/, token: 'comment', next: 'comment'},
+ {regex: /[-+\/*=<>!]+/, token: 'operator'},
+ {regex: /[a-z$][\w$]*/, token: 'variable'},
+ ],
+ // The multi-line comment state.
+ comment: [
+ {regex: /.*?\*\//, token: 'comment', next: 'start'},
+ {regex: /.*/, token: 'comment'},
+ ],
+ // The meta property contains global information about the mode. It
+ // can contain properties like lineComment, which are supported by
+ // all modes, and also directives like dontIndentStates, which are
+ // specific to simple modes.
+ meta: {
+ dontIndentStates: ['comment'],
+ lineComment: '//',
+ },
+})
diff --git a/ui/src/index.js b/ui/src/index.js
index 0cc35d03a6..8a8838dfc5 100644
--- a/ui/src/index.js
+++ b/ui/src/index.js
@@ -23,6 +23,7 @@ import {
KapacitorRulePage,
KapacitorRulesPage,
KapacitorTasksPage,
+ TickscriptPage,
} from 'src/kapacitor'
import {AdminPage} from 'src/admin'
import {CreateSource, SourcePage, ManageSources} from 'src/sources'
@@ -140,6 +141,8 @@ const Root = React.createClass({
+
+
diff --git a/ui/src/kapacitor/actions/view/index.js b/ui/src/kapacitor/actions/view/index.js
index 85d92dfcaa..20914ac606 100644
--- a/ui/src/kapacitor/actions/view/index.js
+++ b/ui/src/kapacitor/actions/view/index.js
@@ -3,9 +3,11 @@ import {getActiveKapacitor} from 'shared/apis'
import {publishNotification} from 'shared/actions/notifications'
import {
getRules,
- getRule,
+ getRule as getRuleAJAX,
deleteRule as deleteRuleAPI,
updateRuleStatus as updateRuleStatusAPI,
+ createTask as createTaskAJAX,
+ updateTask as updateTaskAJAX,
} from 'src/kapacitor/apis'
import {errorThrown} from 'shared/actions/errors'
@@ -19,7 +21,7 @@ const loadQuery = query => ({
export function fetchRule(source, ruleID) {
return dispatch => {
getActiveKapacitor(source).then(kapacitor => {
- getRule(kapacitor, ruleID).then(({data: rule}) => {
+ getRuleAJAX(kapacitor, ruleID).then(({data: rule}) => {
dispatch({
type: 'LOAD_RULE',
payload: {
@@ -39,6 +41,31 @@ const addQuery = queryID => ({
},
})
+export const getRule = (kapacitor, ruleID) => async dispatch => {
+ try {
+ const {data: rule} = await getRuleAJAX(kapacitor, ruleID)
+
+ dispatch({
+ type: 'LOAD_RULE',
+ payload: {
+ rule: {...rule, queryID: rule.query && rule.query.id},
+ },
+ })
+
+ if (rule.query) {
+ dispatch({
+ type: 'LOAD_KAPACITOR_QUERY',
+ payload: {
+ query: rule.query,
+ },
+ })
+ }
+ } catch (error) {
+ console.error(error)
+ throw error
+ }
+}
+
export function loadDefaultRule() {
return dispatch => {
const queryID = uuid.v4()
@@ -208,3 +235,46 @@ export function updateRuleStatus(rule, status) {
})
}
}
+
+export const createTask = (
+ kapacitor,
+ task,
+ router,
+ sourceID
+) => async dispatch => {
+ try {
+ const {data} = await createTaskAJAX(kapacitor, task)
+ router.push(`/sources/${sourceID}/alert-rules`)
+ dispatch(publishNotification('success', 'You made a TICKscript!'))
+ return data
+ } catch (error) {
+ if (!error) {
+ dispatch(errorThrown('Could not communicate with server'))
+ return
+ }
+
+ return error.data
+ }
+}
+
+export const updateTask = (
+ kapacitor,
+ task,
+ ruleID,
+ router,
+ sourceID
+) => async dispatch => {
+ try {
+ const {data} = await updateTaskAJAX(kapacitor, task, ruleID, sourceID)
+ router.push(`/sources/${sourceID}/alert-rules`)
+ dispatch(publishNotification('success', 'TICKscript updated successully'))
+ return data
+ } catch (error) {
+ if (!error) {
+ dispatch(errorThrown('Could not communicate with server'))
+ return
+ }
+
+ return error.data
+ }
+}
diff --git a/ui/src/kapacitor/apis/index.js b/ui/src/kapacitor/apis/index.js
index 8d1959e0ff..d24847f279 100644
--- a/ui/src/kapacitor/apis/index.js
+++ b/ui/src/kapacitor/apis/index.js
@@ -1,6 +1,6 @@
import AJAX from 'utils/ajax'
-function rangeRule(rule) {
+const rangeRule = rule => {
const {value, rangeValue, operator} = rule.values
if (operator === 'inside range' || operator === 'outside range') {
@@ -11,7 +11,7 @@ function rangeRule(rule) {
return rule
}
-export function createRule(kapacitor, rule) {
+export const createRule = (kapacitor, rule) => {
return AJAX({
method: 'POST',
url: kapacitor.links.rules,
@@ -19,21 +19,26 @@ export function createRule(kapacitor, rule) {
})
}
-export function getRules(kapacitor) {
+export const getRules = kapacitor => {
return AJAX({
method: 'GET',
url: kapacitor.links.rules,
})
}
-export function getRule(kapacitor, ruleID) {
- return AJAX({
- method: 'GET',
- url: `${kapacitor.links.rules}/${ruleID}`,
- })
+export const getRule = async (kapacitor, ruleID) => {
+ try {
+ return await AJAX({
+ method: 'GET',
+ url: `${kapacitor.links.rules}/${ruleID}`,
+ })
+ } catch (error) {
+ console.error(error)
+ throw error
+ }
}
-export function editRule(rule) {
+export const editRule = rule => {
return AJAX({
method: 'PUT',
url: rule.links.self,
@@ -41,17 +46,57 @@ export function editRule(rule) {
})
}
-export function deleteRule(rule) {
+export const deleteRule = rule => {
return AJAX({
method: 'DELETE',
url: rule.links.self,
})
}
-export function updateRuleStatus(rule, status) {
+export const updateRuleStatus = (rule, status) => {
return AJAX({
method: 'PATCH',
url: rule.links.self,
data: {status},
})
}
+
+export const createTask = async (kapacitor, {id, dbrps, tickscript, type}) => {
+ try {
+ return await AJAX({
+ method: 'POST',
+ url: kapacitor.links.rules,
+ data: {
+ id,
+ type,
+ dbrps,
+ tickscript,
+ },
+ })
+ } catch (error) {
+ console.error(error)
+ throw error
+ }
+}
+
+export const updateTask = async (
+ kapacitor,
+ {id, dbrps, tickscript, type},
+ ruleID
+) => {
+ try {
+ return await AJAX({
+ method: 'PUT',
+ url: `${kapacitor.links.rules}/${ruleID}`,
+ data: {
+ id,
+ type,
+ dbrps,
+ tickscript,
+ },
+ })
+ } catch (error) {
+ console.error(error)
+ throw error
+ }
+}
diff --git a/ui/src/kapacitor/components/KapacitorRules.js b/ui/src/kapacitor/components/KapacitorRules.js
index f6e6c2f489..611c36e0f1 100644
--- a/ui/src/kapacitor/components/KapacitorRules.js
+++ b/ui/src/kapacitor/components/KapacitorRules.js
@@ -4,8 +4,8 @@ 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 TICKscriptOverlay from 'src/kapacitor/components/TICKscriptOverlay'
const KapacitorRules = ({
source,
@@ -13,10 +13,7 @@ const KapacitorRules = ({
hasKapacitor,
loading,
onDelete,
- tickscript,
onChangeRuleStatus,
- onReadTickscript,
- onCloseTickscript,
}) => {
if (loading) {
return (
@@ -44,43 +41,72 @@ const KapacitorRules = ({
)
}
- const tableHeader =
- rules.length === 1 ? '1 Alert Rule' : `${rules.length} Alert Rules`
+ const rulez = rules.filter(r => r.query)
+ const tasks = rules.filter(r => !r.query)
+
+ const rHeader = `${rulez.length} Alert Rule${rulez.length === 1 ? '' : 's'}`
+ const tHeader = `${tasks.length} TICKscript${tasks.length === 1 ? '' : 's'}`
+
return (
-
+
- {tableHeader}
+ {rHeader}
-
- Create Rule
-
+
+
+ Build Rule
+
+
+
+
+
+
+
+
+ {tHeader}
+
+
+
+ Write TICKscript
+
+
+
+
+
+
+
)
}
-const PageContents = ({children, source, tickscript, onCloseTickscript}) =>
+const PageContents = ({children, source}) =>
- Alert Rules
+
+ Build Alert Rules or Write TICKscripts
+
@@ -98,15 +124,9 @@ const PageContents = ({children, source, tickscript, onCloseTickscript}) =>
- {tickscript
- ?
- : null}
-const {arrayOf, bool, func, node, shape, string} = PropTypes
+const {arrayOf, bool, func, node, shape} = PropTypes
KapacitorRules.propTypes = {
source: shape(),
@@ -115,15 +135,11 @@ KapacitorRules.propTypes = {
loading: bool,
onChangeRuleStatus: func,
onDelete: func,
- tickscript: string,
- onReadTickscript: func,
- onCloseTickscript: func,
}
PageContents.propTypes = {
children: node,
source: shape(),
- tickscript: string,
onCloseTickscript: func,
}
diff --git a/ui/src/kapacitor/components/KapacitorRulesTable.js b/ui/src/kapacitor/components/KapacitorRulesTable.js
index 8b3d944cfd..00f6ab2689 100644
--- a/ui/src/kapacitor/components/KapacitorRulesTable.js
+++ b/ui/src/kapacitor/components/KapacitorRulesTable.js
@@ -5,26 +5,20 @@ import _ from 'lodash'
import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing'
const {
colName,
- colType,
+ colTrigger,
colMessage,
colAlerts,
colEnabled,
colActions,
} = KAPACITOR_RULES_TABLE
-const KapacitorRulesTable = ({
- rules,
- source,
- onDelete,
- onReadTickscript,
- onChangeRuleStatus,
-}) =>
+const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
Name |
- Rule Type |
+ Rule Trigger |
Message |
Alerts |
@@ -42,7 +36,6 @@ const KapacitorRulesTable = ({
source={source}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
- onRead={onReadTickscript}
/>
)
})}
@@ -50,12 +43,14 @@ const KapacitorRulesTable = ({
|
-const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
+const handleDelete = (rule, onDelete) => onDelete(rule)
+
+const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
|
-
+ |
{rule.trigger}
|
@@ -82,10 +77,16 @@ const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
|
-
- |
@@ -117,7 +118,6 @@ KapacitorRulesTable.propTypes = {
source: shape({
id: string.isRequired,
}).isRequired,
- onReadTickscript: func,
}
RuleRow.propTypes = {
@@ -125,7 +125,6 @@ RuleRow.propTypes = {
source: shape(),
onChangeRuleStatus: func,
onDelete: func,
- onRead: func,
}
RuleTitle.propTypes = {
diff --git a/ui/src/kapacitor/components/TasksTable.js b/ui/src/kapacitor/components/TasksTable.js
new file mode 100644
index 0000000000..75784e508d
--- /dev/null
+++ b/ui/src/kapacitor/components/TasksTable.js
@@ -0,0 +1,96 @@
+import React, {PropTypes} from 'react'
+import {Link} from 'react-router'
+import _ from 'lodash'
+
+import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
+
+const {colID, colType, colEnabled, colActions} = TASKS_TABLE
+
+const TasksTable = ({tasks, source, onDelete, onChangeRuleStatus}) =>
+
+
+
+
+ ID |
+ Type |
+
+ Enabled
+ |
+ |
+
+
+
+ {_.sortBy(tasks, t => t.name.toLowerCase()).map(task => {
+ return (
+
+ )
+ })}
+
+
+
+
+const handleDelete = (task, onDelete) => onDelete(task)
+
+const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) =>
+
+
+
+ {task.id}
+
+ |
+
+ {task.type}
+ |
+
+
+
+
+
+ |
+
+
+ Edit TICKscript
+
+
+ Delete
+
+ |
+
+
+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
diff --git a/ui/src/kapacitor/components/Tickscript.js b/ui/src/kapacitor/components/Tickscript.js
new file mode 100644
index 0000000000..539d02fb04
--- /dev/null
+++ b/ui/src/kapacitor/components/Tickscript.js
@@ -0,0 +1,65 @@
+import React, {PropTypes} from 'react'
+import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
+import TickscriptEditor from 'src/kapacitor/components/TickscriptEditor'
+
+const Tickscript = ({
+ source,
+ onSave,
+ task,
+ validation,
+ onSelectDbrps,
+ onChangeScript,
+ onChangeType,
+ onChangeID,
+ isNewTickscript,
+}) =>
+
+
+
+
+
+ {validation
+ ?
+ {validation}
+
+ :
+ Save your TICKscript to validate it
+ }
+
+
+
+
+
+
+
+
+const {arrayOf, bool, func, shape, string} = PropTypes
+
+Tickscript.propTypes = {
+ onSave: func.isRequired,
+ source: shape(),
+ task: shape({
+ id: string,
+ script: string,
+ dbsrps: arrayOf(shape()),
+ }).isRequired,
+ onChangeScript: func.isRequired,
+ onSelectDbrps: func.isRequired,
+ validation: string,
+ onChangeType: func.isRequired,
+ onChangeID: func.isRequired,
+ isNewTickscript: bool.isRequired,
+}
+
+export default Tickscript
diff --git a/ui/src/kapacitor/components/TickscriptEditor.js b/ui/src/kapacitor/components/TickscriptEditor.js
new file mode 100644
index 0000000000..0ade89c9ad
--- /dev/null
+++ b/ui/src/kapacitor/components/TickscriptEditor.js
@@ -0,0 +1,36 @@
+import React, {PropTypes, Component} from 'react'
+import CodeMirror from '@skidding/react-codemirror'
+import 'src/external/codemirror'
+
+class TickscriptEditor extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ updateCode = script => {
+ this.props.onChangeScript(script)
+ }
+
+ render() {
+ const {script} = this.props
+
+ const options = {
+ lineNumbers: true,
+ theme: 'material',
+ tabIndex: 1,
+ }
+
+ return (
+
+ )
+ }
+}
+
+const {func, string} = PropTypes
+
+TickscriptEditor.propTypes = {
+ onChangeScript: func,
+ script: string,
+}
+
+export default TickscriptEditor
diff --git a/ui/src/kapacitor/components/TickscriptHeader.js b/ui/src/kapacitor/components/TickscriptHeader.js
new file mode 100644
index 0000000000..c6dcaee71d
--- /dev/null
+++ b/ui/src/kapacitor/components/TickscriptHeader.js
@@ -0,0 +1,66 @@
+import React, {PropTypes} from 'react'
+import SourceIndicator from 'shared/components/SourceIndicator'
+import TickscriptType from 'src/kapacitor/components/TickscriptType'
+import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
+import TickscriptID, {
+ TickscriptStaticID,
+} from 'src/kapacitor/components/TickscriptID'
+
+const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
+
+const TickscriptHeader = ({
+ task: {id, type, dbrps},
+ task,
+ source: {name},
+ onSave,
+ onChangeType,
+ onChangeID,
+ onSelectDbrps,
+ isNewTickscript,
+}) =>
+
+
+
+ {isNewTickscript
+ ?
+ : }
+
+
+
+
+
+
+ Save Rule
+
+
+
+
+
+const {arrayOf, bool, func, shape, string} = PropTypes
+
+TickscriptHeader.propTypes = {
+ onSave: func,
+ source: shape(),
+ onSelectDbrps: func.isRequired,
+ task: shape({
+ dbrps: arrayOf(
+ shape({
+ db: string,
+ rp: string,
+ })
+ ),
+ }),
+ onChangeType: func.isRequired,
+ onChangeID: func.isRequired,
+ isNewTickscript: bool.isRequired,
+}
+
+export default TickscriptHeader
diff --git a/ui/src/kapacitor/components/TickscriptID.js b/ui/src/kapacitor/components/TickscriptID.js
new file mode 100644
index 0000000000..c005f509b0
--- /dev/null
+++ b/ui/src/kapacitor/components/TickscriptID.js
@@ -0,0 +1,44 @@
+import React, {PropTypes, Component} from 'react'
+
+class TickscriptID extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const {onChangeID, id} = this.props
+
+ return (
+
+ )
+ }
+}
+
+export const TickscriptStaticID = ({id}) =>
+
+ {id}
+
+
+const {func, string} = PropTypes
+
+TickscriptID.propTypes = {
+ onChangeID: func.isRequired,
+ id: string.isRequired,
+}
+
+TickscriptStaticID.propTypes = {
+ id: string.isRequired,
+}
+
+export default TickscriptID
diff --git a/ui/src/kapacitor/components/TickscriptType.js b/ui/src/kapacitor/components/TickscriptType.js
new file mode 100644
index 0000000000..e28a454bc2
--- /dev/null
+++ b/ui/src/kapacitor/components/TickscriptType.js
@@ -0,0 +1,28 @@
+import React, {PropTypes} from 'react'
+const STREAM = 'stream'
+const BATCH = 'batch'
+
+const TickscriptType = ({type, onChangeType}) =>
+
+ -
+ Stream
+
+ -
+ Batch
+
+
+
+const {string, func} = PropTypes
+
+TickscriptType.propTypes = {
+ type: string,
+ onChangeType: func,
+}
+
+export default TickscriptType
diff --git a/ui/src/kapacitor/constants/tableSizing.js b/ui/src/kapacitor/constants/tableSizing.js
index 5d96be7625..4c1a40699b 100644
--- a/ui/src/kapacitor/constants/tableSizing.js
+++ b/ui/src/kapacitor/constants/tableSizing.js
@@ -1,8 +1,15 @@
export const KAPACITOR_RULES_TABLE = {
colName: '200px',
- colType: '90px',
+ colTrigger: '90px',
colMessage: '460px',
colAlerts: '120px',
colEnabled: '64px',
colActions: '176px',
}
+
+export const TASKS_TABLE = {
+ colID: '200px',
+ colType: '90px',
+ colEnabled: '64px',
+ colActions: '176px',
+}
diff --git a/ui/src/kapacitor/containers/KapacitorRulesPage.js b/ui/src/kapacitor/containers/KapacitorRulesPage.js
index 993e9787b6..8652e1fd11 100644
--- a/ui/src/kapacitor/containers/KapacitorRulesPage.js
+++ b/ui/src/kapacitor/containers/KapacitorRulesPage.js
@@ -11,7 +11,6 @@ class KapacitorRulesPage extends Component {
this.state = {
hasKapacitor: false,
loading: true,
- tickscript: null,
}
}
@@ -38,17 +37,9 @@ class KapacitorRulesPage extends Component {
actions.updateRuleStatusSuccess(rule.id, status)
}
- handleReadTickscript = ({tickscript}) => () => {
- this.setState({tickscript})
- }
-
- handleCloseTickscript = () => {
- this.setState({tickscript: null})
- }
-
render() {
const {source, rules} = this.props
- const {hasKapacitor, loading, tickscript} = this.state
+ const {hasKapacitor, loading} = this.state
return (
)
}
diff --git a/ui/src/kapacitor/containers/TickscriptPage.js b/ui/src/kapacitor/containers/TickscriptPage.js
new file mode 100644
index 0000000000..b882ecf8a1
--- /dev/null
+++ b/ui/src/kapacitor/containers/TickscriptPage.js
@@ -0,0 +1,158 @@
+import React, {PropTypes, Component} from 'react'
+import {connect} from 'react-redux'
+import {bindActionCreators} from 'redux'
+
+import Tickscript from 'src/kapacitor/components/Tickscript'
+import * as kapactiorActionCreators from 'src/kapacitor/actions/view'
+import * as errorActionCreators from 'shared/actions/errors'
+import {getActiveKapacitor} from 'src/shared/apis'
+
+class TickscriptPage extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ kapacitor: {},
+ task: {
+ id: '',
+ name: '',
+ status: 'enabled',
+ tickscript: '',
+ dbrps: [],
+ type: 'stream',
+ },
+ validation: '',
+ isEditingID: true,
+ }
+ }
+
+ async componentDidMount() {
+ const {
+ source,
+ errorActions,
+ kapacitorActions,
+ params: {ruleID},
+ } = this.props
+
+ const kapacitor = await getActiveKapacitor(source)
+ if (!kapacitor) {
+ errorActions.errorThrown(
+ 'We could not find a configured Kapacitor for this source'
+ )
+ }
+
+ if (this._isEditing()) {
+ await kapacitorActions.getRule(kapacitor, ruleID)
+ const {id, name, tickscript, dbrps, type} = this.props.rules.find(
+ r => r.id === ruleID
+ )
+
+ this.setState({task: {tickscript, dbrps, type, status, name, id}})
+ }
+
+ this.setState({kapacitor})
+ }
+
+ handleSave = async () => {
+ const {kapacitor, task} = this.state
+ const {
+ source: {id: sourceID},
+ router,
+ kapacitorActions: {createTask, updateTask},
+ params: {ruleID},
+ } = this.props
+
+ let response
+
+ try {
+ if (this._isEditing()) {
+ response = await updateTask(kapacitor, task, ruleID, router, sourceID)
+ } else {
+ response = await createTask(kapacitor, task, router, sourceID)
+ }
+
+ if (response && response.code === 500) {
+ return this.setState({validation: response.message})
+ }
+ } catch (error) {
+ console.error(error)
+ throw error
+ }
+ }
+
+ handleChangeScript = tickscript => {
+ this.setState({task: {...this.state.task, tickscript}})
+ }
+
+ handleSelectDbrps = dbrps => {
+ this.setState({task: {...this.state.task, dbrps}})
+ }
+
+ handleChangeType = type => () => {
+ this.setState({task: {...this.state.task, type}})
+ }
+
+ handleChangeID = e => {
+ this.setState({task: {...this.state.task, id: e.target.value}})
+ }
+
+ render() {
+ const {source} = this.props
+ const {task, validation} = this.state
+
+ return (
+
+ )
+ }
+
+ _isEditing() {
+ const {params} = this.props
+ return params.ruleID && params.ruleID !== 'new'
+ }
+}
+
+const {arrayOf, func, shape, string} = PropTypes
+
+TickscriptPage.propTypes = {
+ source: shape({
+ name: string,
+ }),
+ errorActions: shape({
+ errorThrown: func.isRequired,
+ }).isRequired,
+ kapacitorActions: shape({
+ updateTask: func.isRequired,
+ createTask: func.isRequired,
+ getRule: func.isRequired,
+ }),
+ router: shape({
+ push: func.isRequired,
+ }).isRequired,
+ params: shape({
+ ruleID: string,
+ }).isRequired,
+ rules: arrayOf(shape()),
+}
+
+const mapStateToProps = state => {
+ return {
+ rules: Object.values(state.rules),
+ }
+}
+
+const mapDispatchToProps = dispatch => ({
+ kapacitorActions: bindActionCreators(kapactiorActionCreators, dispatch),
+ errorActions: bindActionCreators(errorActionCreators, dispatch),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(TickscriptPage)
diff --git a/ui/src/kapacitor/index.js b/ui/src/kapacitor/index.js
index fa44936ffb..b017f9ac3d 100644
--- a/ui/src/kapacitor/index.js
+++ b/ui/src/kapacitor/index.js
@@ -2,9 +2,11 @@ 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,
}
diff --git a/ui/src/shared/apis/index.js b/ui/src/shared/apis/index.js
index f632afc3af..9ab2ac57b3 100644
--- a/ui/src/shared/apis/index.js
+++ b/ui/src/shared/apis/index.js
@@ -54,7 +54,7 @@ export function deleteSource(source) {
export function pingKapacitor(kapacitor) {
return AJAX({
method: 'GET',
- url: `${kapacitor.links.proxy}?path=/kapacitor/v1/ping`,
+ url: kapacitor.links.ping,
})
}
diff --git a/ui/src/shared/components/MultiSelectDBDropdown.js b/ui/src/shared/components/MultiSelectDBDropdown.js
new file mode 100644
index 0000000000..050789a33e
--- /dev/null
+++ b/ui/src/shared/components/MultiSelectDBDropdown.js
@@ -0,0 +1,85 @@
+import React, {PropTypes, Component} from 'react'
+
+import {showDatabases, showRetentionPolicies} from 'shared/apis/metaQuery'
+import showDatabasesParser from 'shared/parsing/showDatabases'
+import showRetentionPoliciesParser from 'shared/parsing/showRetentionPolicies'
+import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
+
+class MultiSelectDBDropdown extends Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ dbrps: [],
+ }
+ }
+
+ componentDidMount() {
+ this._getDbRps()
+ }
+
+ render() {
+ const {dbrps} = this.state
+ const {onApply, selectedItems} = this.props
+ const label = 'Select databases'
+
+ return (
+
+ )
+ }
+
+ _getDbRps = async () => {
+ const {source: {links: {proxy}}} = this.context
+ const {onErrorThrown} = this.props
+
+ try {
+ const {data} = await showDatabases(proxy)
+ const {databases, errors} = showDatabasesParser(data)
+ if (errors.length > 0) {
+ throw errors[0] // only one error can come back from this, but it's returned as an array
+ }
+
+ const response = await showRetentionPolicies(proxy, databases)
+ const dbrps = response.data.results.reduce((acc, result, i) => {
+ const {retentionPolicies} = showRetentionPoliciesParser(result)
+ const db = databases[i]
+
+ const rps = retentionPolicies.map(({name: rp}) => ({
+ db,
+ rp,
+ name: `${db}.${rp}`,
+ }))
+
+ return [...acc, ...rps]
+ }, [])
+
+ this.setState({dbrps})
+ } catch (error) {
+ console.error(error)
+ onErrorThrown(error)
+ }
+ }
+}
+
+const {arrayOf, func, shape, string} = PropTypes
+
+MultiSelectDBDropdown.contextTypes = {
+ source: shape({
+ links: shape({
+ proxy: string.isRequired,
+ }).isRequired,
+ }).isRequired,
+}
+
+MultiSelectDBDropdown.propTypes = {
+ onErrorThrown: func,
+ onApply: func.isRequired,
+ selectedItems: arrayOf(shape()),
+}
+
+export default MultiSelectDBDropdown
diff --git a/ui/src/shared/components/MultiSelectDropdown.js b/ui/src/shared/components/MultiSelectDropdown.js
index d279e6fc9d..5abbd159b3 100644
--- a/ui/src/shared/components/MultiSelectDropdown.js
+++ b/ui/src/shared/components/MultiSelectDropdown.js
@@ -8,16 +8,19 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
const labelText = ({localSelectedItems, isOpen, label}) => {
+ if (localSelectedItems.length) {
+ return localSelectedItems.map(s => s.name).join(', ')
+ }
+
if (label) {
return label
- } else if (localSelectedItems.length) {
- return localSelectedItems.map(s => s).join(', ')
}
// TODO: be smarter about the text displayed here
if (isOpen) {
return '0 Selected'
}
+
return 'None'
}
@@ -27,42 +30,52 @@ class MultiSelectDropdown extends Component {
this.state = {
isOpen: false,
- localSelectedItems: this.props.selectedItems,
+ localSelectedItems: props.selectedItems,
}
-
- this.onSelect = ::this.onSelect
- this.onApplyFunctions = ::this.onApplyFunctions
}
handleClickOutside() {
this.setState({isOpen: false})
}
+ componentWillReceiveProps(nextProps) {
+ if (!_.isEqual(this.props.selectedItems, nextProps.selectedItems)) {
+ return
+ }
+
+ this.setState({localSelectedItems: nextProps.selectedItems})
+ }
+
toggleMenu = e => {
e.stopPropagation()
this.setState({isOpen: !this.state.isOpen})
}
- onSelect(item, e) {
+ onSelect = (item, e) => {
e.stopPropagation()
+ const {onApply, isApplyShown} = this.props
const {localSelectedItems} = this.state
let nextItems
if (this.isSelected(item)) {
- nextItems = localSelectedItems.filter(i => i !== item)
+ nextItems = localSelectedItems.filter(i => i.name !== item.name)
} else {
nextItems = [...localSelectedItems, item]
}
+ if (!isApplyShown) {
+ onApply(nextItems)
+ }
+
this.setState({localSelectedItems: nextItems})
}
isSelected(item) {
- return !!this.state.localSelectedItems.find(text => text === item)
+ return !!this.state.localSelectedItems.find(({name}) => name === item.name)
}
- onApplyFunctions(e) {
+ handleApply = e => {
e.stopPropagation()
this.setState({isOpen: false})
@@ -91,18 +104,18 @@ class MultiSelectDropdown extends Component {
}
renderMenu() {
- const {items} = this.props
-
- return (
-
- -
-
+ const {items, isApplyShown} = this.props
+ const applyButton = isApplyShown
+ ?
-
+
Apply
+ : null
+
+ return (
+
+ {applyButton}
- {listItem}
+ {listItem.name}
)
@@ -130,22 +143,34 @@ class MultiSelectDropdown extends Component {
}
}
-const {arrayOf, func, string} = PropTypes
+const {arrayOf, bool, func, shape, string} = PropTypes
MultiSelectDropdown.propTypes = {
onApply: func.isRequired,
- items: arrayOf(string.isRequired).isRequired,
- selectedItems: arrayOf(string.isRequired).isRequired,
+ items: arrayOf(
+ shape({
+ name: string.isRequired,
+ })
+ ).isRequired,
+ selectedItems: arrayOf(
+ shape({
+ name: string.isRequired,
+ })
+ ),
label: string,
buttonSize: string,
buttonColor: string,
customClass: string,
iconName: string,
+ isApplyShown: bool,
}
+
MultiSelectDropdown.defaultProps = {
buttonSize: 'btn-sm',
buttonColor: 'btn-default',
customClass: 'dropdown-160',
+ selectedItems: [],
+ isApplyShown: true,
}
export default OnClickOutside(MultiSelectDropdown)
diff --git a/ui/src/side_nav/containers/SideNav.js b/ui/src/side_nav/containers/SideNav.js
index 7b8597470d..a15293b46b 100644
--- a/ui/src/side_nav/containers/SideNav.js
+++ b/ui/src/side_nav/containers/SideNav.js
@@ -102,11 +102,9 @@ const SideNav = React.createClass({
link={`${sourcePrefix}/alerts`}
>
-
- Alert History
-
+ History
- Alert Rules
+ Create
diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss
index a8bc1d7191..d7ec2232fc 100644
--- a/ui/src/style/chronograf.scss
+++ b/ui/src/style/chronograf.scss
@@ -11,11 +11,13 @@
@import 'theme/bootstrap-theme';
@import 'theme/reset';
-// External
+// Vendor
@import 'external/react-grid-layout';
@import 'external/fixed-data-table-base';
@import 'external/fixed-data-table-style';
@import 'external/fixed-data-table';
+@import "external/codemirror";
+@import "../../node_modules/codemirror/theme/material.css";
// Layout
@import 'layout/page';
@@ -27,6 +29,7 @@
// Components
@import 'components/ceo-display-options';
@import 'components/confirm-buttons';
+@import 'components/code-mirror-theme';
@import 'components/custom-time-range';
@import 'components/dygraphs';
@import 'components/fancy-scrollbars';
diff --git a/ui/src/style/components/code-mirror-theme.scss b/ui/src/style/components/code-mirror-theme.scss
new file mode 100644
index 0000000000..a067d11453
--- /dev/null
+++ b/ui/src/style/components/code-mirror-theme.scss
@@ -0,0 +1,124 @@
+/*
+
+ Name: CHRONOGRAF YO
+ Author: Michael Kaminsky (http://github.com/mkaminsky11)
+
+ Original material color scheme by Mattia Astorino (https://github.com/equinusocio/material-theme)
+
+*/
+
+$tickscript-console-height: 120px;
+
+.tickscript-console,
+.tickscript-editor {
+ padding-left: $page-wrapper-padding;
+ padding-right: $page-wrapper-padding;
+ margin: 0 auto;
+ max-width: $page-wrapper-max-width;
+ position: relative;
+}
+.tickscript-console {
+ height: $tickscript-console-height;
+ padding-top: 30px;
+}
+.tickscript-console--output {
+ padding: 0 60px;
+ font-family: $code-font;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ background-color: $g3-castle;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ border-radius: $radius $radius 0 0;
+
+ > p {
+ margin: 0;
+ }
+}
+.tickscript-console--default {
+ color: $g10-wolf;
+ font-style: italic;
+}
+.tickscript-editor {
+ margin: 0 auto;
+ padding-bottom: 30px;
+ height: calc(100% - #{$tickscript-console-height});
+}
+.ReactCodeMirror {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+.cm-s-material.CodeMirror {
+ border-radius: 0 0 $radius $radius;
+ font-family: $code-font;
+ background-color: $g2-kevlar;
+ color: $c-neutrino;
+ font-weight: 600;
+ height: 100%;
+}
+.CodeMirror-vscrollbar {
+ @include custom-scrollbar-round($g2-kevlar,$g6-smoke);
+}
+.cm-s-material .CodeMirror-gutters {
+ background-color: fade-out($g4-onyx, 0.5);
+ border: none;
+}
+.CodeMirror-gutter.CodeMirror-linenumbers {
+ width: 60px;
+}
+.cm-s-material.CodeMirror .CodeMirror-sizer {
+ margin-left: 60px;
+}
+.cm-s-material.CodeMirror .CodeMirror-linenumber.CodeMirror-gutter-elt {
+ padding-right: 9px;
+ width: 46px;
+ color: $g8-storm;
+}
+.cm-s-material .CodeMirror-guttermarker, .cm-s-material .CodeMirror-guttermarker-subtle, .cm-s-material .CodeMirror-linenumber { color: rgb(83,127,126); }
+.cm-s-material .CodeMirror-cursor {
+ width: 2px;
+ border: 0;
+ background-color: $g20-white;
+ box-shadow:
+ 0 0 3px $c-laser,
+ 0 0 6px $c-ocean,
+ 0 0 11px $c-amethyst;
+}
+.cm-s-material div.CodeMirror-selected,
+.cm-s-material.CodeMirror-focused div.CodeMirror-selected {
+ background-color: fade-out($g8-storm,0.7);
+}
+.cm-s-material .CodeMirror-line::selection, .cm-s-material .CodeMirror-line > span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }
+.cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }
+
+.cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); }
+.cm-s-material .cm-keyword { color: $c-comet; }
+.cm-s-material .cm-operator { color: $c-dreamsicle; }
+.cm-s-material .cm-variable-2 { color: #80CBC4; }
+.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: $c-laser; }
+.cm-s-material .cm-builtin { color: #DECB6B; }
+.cm-s-material .cm-atom { color: $c-viridian; }
+.cm-s-material .cm-number { color: $c-daisy; }
+.cm-s-material .cm-def { color: rgba(233, 237, 237, 1); }
+.cm-s-material .cm-string { color: $c-krypton; }
+.cm-s-material .cm-string-2 { color: #80CBC4; }
+.cm-s-material .cm-comment { color: $g10-wolf; }
+.cm-s-material .cm-variable { color: $c-laser; }
+.cm-s-material .cm-tag { color: #80CBC4; }
+.cm-s-material .cm-meta { color: #80CBC4; }
+.cm-s-material .cm-attribute { color: #FFCB6B; }
+.cm-s-material .cm-property { color: #80CBAE; }
+.cm-s-material .cm-qualifier { color: #DECB6B; }
+.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #DECB6B; }
+.cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); }
+.cm-s-material .cm-error {
+ color: rgba(255, 255, 255, 1.0);
+ background-color: #EC5F67;
+}
+.cm-s-material .CodeMirror-matchingbracket {
+ text-decoration: underline;
+ color: white !important;
+}
diff --git a/ui/src/style/external/codemirror.scss b/ui/src/style/external/codemirror.scss
new file mode 100644
index 0000000000..935821d4a1
--- /dev/null
+++ b/ui/src/style/external/codemirror.scss
@@ -0,0 +1,340 @@
+/* BASICS */
+
+.CodeMirror {
+ /* Set height, width, borders, and global font properties here */
+ font-family: monospace;
+ height: calc(100vh - 140px);
+ color: black;
+}
+
+/* PADDING */
+
+.CodeMirror-lines {
+ padding: 4px 0; /* Vertical padding around content */
+}
+.CodeMirror pre {
+ padding: 0 4px; /* Horizontal padding of content */
+}
+
+.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ background-color: white; /* The little square between H and V scrollbars */
+}
+
+/* GUTTER */
+
+.CodeMirror-gutters {
+ border-right: 1px solid #ddd;
+ background-color: #f7f7f7;
+ white-space: nowrap;
+}
+.CodeMirror-linenumbers {}
+.CodeMirror-linenumber {
+ padding: 0 3px 0 5px;
+ min-width: 20px;
+ text-align: right;
+ color: #999;
+ white-space: nowrap;
+}
+
+.CodeMirror-guttermarker { color: black; }
+.CodeMirror-guttermarker-subtle { color: #999; }
+
+/* CURSOR */
+
+.CodeMirror-cursor {
+ border-left: 1px solid black;
+ border-right: none;
+ width: 0;
+}
+/* Shown when moving in bi-directional text */
+.CodeMirror div.CodeMirror-secondarycursor {
+ border-left: 1px solid silver;
+}
+.cm-fat-cursor .CodeMirror-cursor {
+ width: auto;
+ border: 0 !important;
+ background: #7e7;
+}
+.cm-fat-cursor div.CodeMirror-cursors {
+ z-index: 1;
+}
+
+.cm-animate-fat-cursor {
+ width: auto;
+ border: 0;
+ -webkit-animation: blink 1.06s steps(1) infinite;
+ -moz-animation: blink 1.06s steps(1) infinite;
+ animation: blink 1.06s steps(1) infinite;
+ background-color: #7e7;
+}
+@-moz-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@-webkit-keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+@keyframes blink {
+ 0% {}
+ 50% { background-color: transparent; }
+ 100% {}
+}
+
+/* Can style cursor different in overwrite (non-insert) mode */
+.CodeMirror-overwrite .CodeMirror-cursor {}
+
+.cm-tab { display: inline-block; text-decoration: inherit; }
+
+.CodeMirror-rulers {
+ position: absolute;
+ left: 0; right: 0; top: -50px; bottom: -20px;
+ overflow: hidden;
+}
+.CodeMirror-ruler {
+ border-left: 1px solid #ccc;
+ top: 0; bottom: 0;
+ position: absolute;
+}
+
+/* DEFAULT THEME */
+
+.cm-s-default .cm-header {color: blue;}
+.cm-s-default .cm-quote {color: #090;}
+.cm-negative {color: #d44;}
+.cm-positive {color: #292;}
+.cm-header, .cm-strong {font-weight: bold;}
+.cm-em {font-style: italic;}
+.cm-link {text-decoration: underline;}
+.cm-strikethrough {text-decoration: line-through;}
+
+.cm-s-default .cm-keyword {color: #708;}
+.cm-s-default .cm-atom {color: #219;}
+.cm-s-default .cm-number {color: #164;}
+.cm-s-default .cm-def {color: #00f;}
+.cm-s-default .cm-variable,
+.cm-s-default .cm-punctuation,
+.cm-s-default .cm-property,
+.cm-s-default .cm-operator {}
+.cm-s-default .cm-variable-2 {color: #05a;}
+.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
+.cm-s-default .cm-comment {color: #a50;}
+.cm-s-default .cm-string {color: #a11;}
+.cm-s-default .cm-string-2 {color: #f50;}
+.cm-s-default .cm-meta {color: #555;}
+.cm-s-default .cm-qualifier {color: #555;}
+.cm-s-default .cm-builtin {color: #30a;}
+.cm-s-default .cm-bracket {color: #997;}
+.cm-s-default .cm-tag {color: #170;}
+.cm-s-default .cm-attribute {color: #00c;}
+.cm-s-default .cm-hr {color: #999;}
+.cm-s-default .cm-link {color: #00c;}
+
+.cm-s-default .cm-error {color: #f00;}
+.cm-invalidchar {color: #f00;}
+
+.CodeMirror-composing { border-bottom: 2px solid; }
+
+/* Default styles for common addons */
+
+div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
+div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
+.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
+.CodeMirror-activeline-background {background: #e8f2ff;}
+
+/* STOP */
+
+/* The rest of this file contains styles related to the mechanics of
+ the editor. You probably shouldn't touch them. */
+
+.CodeMirror {
+ position: relative;
+ overflow: hidden;
+ background: white;
+}
+
+.CodeMirror-scroll {
+ overflow: scroll !important; /* Things will break if this is overridden */
+ /* 30px is the magic margin used to hide the element's real scrollbars */
+ /* See overflow: hidden in .CodeMirror */
+ margin-bottom: -30px; margin-right: -30px;
+ padding-bottom: 30px;
+ height: 100%;
+ outline: none; /* Prevent dragging from highlighting the element */
+ position: relative;
+}
+.CodeMirror-sizer {
+ position: relative;
+ border-right: 30px solid transparent;
+}
+
+/* The fake, visible scrollbars. Used to force redraw during scrolling
+ before actual scrolling happens, thus preventing shaking and
+ flickering artifacts. */
+.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
+ position: absolute;
+ z-index: 6;
+ display: none;
+}
+.CodeMirror-vscrollbar {
+ right: 0; top: 0;
+ overflow-x: hidden;
+ overflow-y: scroll;
+}
+.CodeMirror-hscrollbar {
+ bottom: 0; left: 0;
+ overflow-y: hidden;
+ overflow-x: scroll;
+}
+.CodeMirror-scrollbar-filler {
+ right: 0; bottom: 0;
+}
+.CodeMirror-gutter-filler {
+ left: 0; bottom: 0;
+}
+
+.CodeMirror-gutters {
+ position: absolute; left: 0; top: 0;
+ min-height: 100%;
+ z-index: 3;
+}
+.CodeMirror-gutter {
+ white-space: normal;
+ height: 100%;
+ display: inline-block;
+ vertical-align: top;
+ margin-bottom: -30px;
+}
+.CodeMirror-gutter-wrapper {
+ position: absolute;
+ z-index: 4;
+ background: none !important;
+ border: none !important;
+}
+.CodeMirror-gutter-background {
+ position: absolute;
+ top: 0; bottom: 0;
+ z-index: 4;
+}
+.CodeMirror-gutter-elt {
+ position: absolute;
+ cursor: default;
+ z-index: 4;
+}
+.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
+.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
+
+.CodeMirror-lines {
+ cursor: text;
+ min-height: 1px; /* prevents collapsing before first draw */
+}
+.CodeMirror pre {
+ /* Reset some styles that the rest of the page might have set */
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
+ border-width: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: inherit;
+ margin: 0;
+ white-space: pre;
+ word-wrap: normal;
+ line-height: inherit;
+ color: inherit;
+ z-index: 2;
+ position: relative;
+ overflow: visible;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-font-variant-ligatures: contextual;
+ font-variant-ligatures: contextual;
+}
+.CodeMirror-wrap pre {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ word-break: normal;
+}
+
+.CodeMirror-linebackground {
+ position: absolute;
+ left: 0; right: 0; top: 0; bottom: 0;
+ z-index: 0;
+}
+
+.CodeMirror-linewidget {
+ position: relative;
+ z-index: 2;
+ overflow: auto;
+}
+
+.CodeMirror-widget {}
+
+.CodeMirror-rtl pre { direction: rtl; }
+
+.CodeMirror-code {
+ outline: none;
+}
+
+/* Force content-box sizing for the elements where we expect it */
+.CodeMirror-scroll,
+.CodeMirror-sizer,
+.CodeMirror-gutter,
+.CodeMirror-gutters,
+.CodeMirror-linenumber {
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+
+.CodeMirror-measure {
+ position: absolute;
+ width: 100%;
+ height: 0;
+ overflow: hidden;
+ visibility: hidden;
+}
+
+.CodeMirror-cursor {
+ position: absolute;
+ pointer-events: none;
+}
+.CodeMirror-measure pre { position: static; }
+
+div.CodeMirror-cursors {
+ visibility: hidden;
+ position: relative;
+ z-index: 3;
+}
+div.CodeMirror-dragcursors {
+ visibility: visible;
+}
+
+.CodeMirror-focused div.CodeMirror-cursors {
+ visibility: visible;
+}
+
+.CodeMirror-selected { background: #d9d9d9; }
+.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+.CodeMirror-crosshair { cursor: crosshair; }
+.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
+.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
+
+.cm-searching {
+ background: #ffa;
+ background: rgba(255, 255, 0, .4);
+}
+
+/* Used to force a border model for a node */
+.cm-force-border { padding-right: .1px; }
+
+@media print {
+ /* Hide the cursor when printing */
+ .CodeMirror div.CodeMirror-cursors {
+ visibility: hidden;
+ }
+}
+
+/* See issue #2901 */
+.cm-tab-wrap-hack:after { content: ''; }
+
+/* Help users use markselection to safely style text background */
+span.CodeMirror-selectedtext { background: none; }
diff --git a/ui/yarn.lock b/ui/yarn.lock
index af7e628795..fc92c6a639 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -98,6 +98,16 @@
webpack-dev-middleware "^1.6.0"
webpack-hot-middleware "^2.13.2"
+"@skidding/react-codemirror@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@skidding/react-codemirror/-/react-codemirror-1.0.1.tgz#ce7927a10248e2369f8bce03669b92d88fead797"
+ dependencies:
+ classnames "^2.2.5"
+ codemirror "^5.18.2"
+ create-react-class "^15.5.1"
+ lodash.isequal "^4.5.0"
+ prop-types "^15.5.4"
+
abab@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d"
@@ -1794,6 +1804,10 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+codemirror@^5.18.2:
+ version "5.28.0"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.28.0.tgz#2978d9280d671351a4f5737d06bbd681a0fd6f83"
+
color-convert@^1.3.0:
version "1.8.2"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.8.2.tgz#be868184d7c8631766d54e7078e2672d7c7e3339"
@@ -4402,6 +4416,10 @@ lodash.debounce@^3.1.1:
dependencies:
lodash._getnative "^3.0.0"
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+
lodash.deburr@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-3.2.0.tgz#6da8f54334a366a7cf4c4c76ef8d80aa1b365ed5"
@@ -4462,6 +4480,10 @@ lodash.isequal@^4.0.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.4.0.tgz#6295768e98e14dc15ce8d362ef6340db82852031"
+lodash.isequal@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+
lodash.keys@^3.0.0, lodash.keys@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -4559,6 +4581,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0:
dependencies:
js-tokens "^2.0.0"
+loose-envify@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
@@ -5814,6 +5842,13 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
+prop-types@^15.5.4:
+ version "15.5.10"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
+ dependencies:
+ fbjs "^0.8.9"
+ loose-envify "^1.3.1"
+
prop-types@^15.5.6, prop-types@^15.5.8:
version "15.5.8"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
@@ -5932,6 +5967,21 @@ react-addons-test-utils@^15.0.2:
version "15.4.1"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.4.1.tgz#1e4caab151bf27cce26df5f9cb714f4fd8359ae1"
+react-addons-update@^15.1.0:
+ version "15.4.1"
+ resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.4.1.tgz#00c07f45243aa9715e1706bbfd1f23d3d8d80bd1"
+
+react-codemirror@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/react-codemirror/-/react-codemirror-1.0.0.tgz#91467b53b1f5d80d916a2fd0b4c7adb85a9001ba"
+ dependencies:
+ classnames "^2.2.5"
+ codemirror "^5.18.2"
+ create-react-class "^15.5.1"
+ lodash.debounce "^4.0.8"
+ lodash.isequal "^4.5.0"
+ prop-types "^15.5.4"
+
react-custom-scrollbars@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.1.1.tgz#cf08cd43b1297ab11e6fcc5c9a800e7b70b6f248"
| | | |