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}) =>
- +
NameRule TypeRule 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}) => +
+ + + + + + + + + + {_.sortBy(tasks, t => t.name.toLowerCase()).map(task => { + return ( + + ) + })} + +
IDType + Enabled + +
+
+ +const handleDelete = (task, onDelete) => onDelete(task) + +const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) => + + + + {task.id} + + + + {task.type} + + +
+ +
+ + + + Edit TICKscript + + + + + +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 + ? + : } +
+
+ + + + +
+
+
+ +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 ( -
    -
  • -
  • + : 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"